From 5c6acb0909677201e768b22ca104fd96fbb2a278 Mon Sep 17 00:00:00 2001 From: Denis Rekalov Date: Thu, 17 Sep 2020 10:15:46 +0100 Subject: [PATCH] CORDA-3968: Prepare keystore handling for certificate rotation [OS] (#6698) * CORDA-3968: Move keystore logic to KeyStoreHandler. * CORDA-3967: Stop generating node legal identity in runtime. * CORDA-3969: Remove Node CA access at node normal runtime. * CORDA-3968: Fix handling of absent keystore file and wrong password. --- detekt-baseline.xml | 11 +- .../net/corda/node/internal/AbstractNode.kt | 201 +-------- .../corda/node/internal/KeyStoreHandler.kt | 230 ++++++++++ .../node/internal/NodeKeyStoreUtilities.kt | 76 ---- .../subcommands/InitialRegistrationCli.kt | 6 +- .../node/services/config/ConfigUtilities.kt | 24 +- .../identity/PersistentIdentityService.kt | 6 +- .../keys/BasicHSMKeyManagementService.kt | 10 +- .../keys/E2ETestKeyManagementService.kt | 91 ---- .../keys/KeyManagementServiceInternal.kt | 3 +- .../registration/NetworkRegistrationHelper.kt | 45 ++ .../node/internal/KeyStoreHandlerTest.kt | 392 ++++++++++++++++++ .../internal/NodeKeyStoreUtilitiesTest.kt | 157 ------- .../PersistentIdentityServiceTests.kt | 7 +- .../net/corda/testing/node/MockServices.kt | 9 +- .../testing/node/internal/DriverDSLImpl.kt | 5 +- .../node/internal/InternalMockNetwork.kt | 17 +- .../node/internal/MockKeyManagementService.kt | 4 +- 18 files changed, 741 insertions(+), 553 deletions(-) create mode 100644 node/src/main/kotlin/net/corda/node/internal/KeyStoreHandler.kt delete mode 100644 node/src/main/kotlin/net/corda/node/internal/NodeKeyStoreUtilities.kt delete mode 100644 node/src/main/kotlin/net/corda/node/services/keys/E2ETestKeyManagementService.kt create mode 100644 node/src/test/kotlin/net/corda/node/internal/KeyStoreHandlerTest.kt delete mode 100644 node/src/test/kotlin/net/corda/node/internal/NodeKeyStoreUtilitiesTest.kt diff --git a/detekt-baseline.xml b/detekt-baseline.xml index 4417f8759a..2e00681da8 100644 --- a/detekt-baseline.xml +++ b/detekt-baseline.xml @@ -108,7 +108,6 @@ ComplexMethod:ClassCarpenter.kt$ClassCarpenterImpl$ private fun validateSchema(schema: Schema) ComplexMethod:CompatibleTransactionTests.kt$CompatibleTransactionTests$@Test(timeout=300_000) fun `Command visibility tests`() ComplexMethod:ConfigUtilities.kt$// For Iterables figure out the type parameter and apply the same logic as above on the individual elements. private fun Iterable<*>.toConfigIterable(field: Field): Iterable<Any?> - ComplexMethod:ConfigUtilities.kt$// TODO Move this to KeyStoreConfigHelpers. fun MutualSslConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500Name, signingCertificateStore: FileBasedCertificateStoreSupplier, certificatesDirectory: Path, cryptoService: CryptoService? = null) ComplexMethod:ConfigUtilities.kt$@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") // Reflect over the fields of the receiver and generate a value Map that can use to create Config object. private fun Any.toConfigMap(): Map<String, Any> ComplexMethod:ConfigUtilities.kt$private fun convertValue(value: Any): Any ComplexMethod:ConnectionStateMachine.kt$ConnectionStateMachine$override fun onConnectionFinal(event: Event) @@ -215,7 +214,6 @@ ForbiddenComment:AbstractCashSelection.kt$AbstractCashSelection$// TODO: future implementation to retrieve contract states from a Vault BLOB store ForbiddenComment:AbstractCashSelection.kt$AbstractCashSelection$// TODO: make parameters configurable when we get CorDapp configuration. ForbiddenComment:AbstractCashSelection.kt$AbstractCashSelection$// TODO: revisit the back off strategy for contended spending. - ForbiddenComment:AbstractNode.kt$AbstractNode$// TODO: Use configuration to indicate composite key should be used instead of public key for the identity. ForbiddenComment:AbstractNode.kt$AbstractNode$// TODO: We need a good way of handling "nice to have" shutdown events, especially those that deal with the ForbiddenComment:AbstractNode.kt$AbstractNode.<no name provided>$// TODO: Exponential backoff? It should reach max interval of eventHorizon/2. ForbiddenComment:AbstractStateReplacementFlow.kt$AbstractStateReplacementFlow.Acceptor$// TODO: This step should not be necessary, as signatures are re-checked in verifyRequiredSignatures. @@ -307,7 +305,6 @@ ForbiddenComment:DriverDSLImpl.kt$DriverDSLImpl$//TODO: remove this once we can bundle quasar properly. ForbiddenComment:DriverDSLImpl.kt$DriverDSLImpl.LocalNetworkMap$// TODO: this object will copy NodeInfo files from started nodes to other nodes additional-node-infos/ ForbiddenComment:DummyFungibleContract.kt$DummyFungibleContract$// TODO: This doesn't work with the trader demo, so use the underlying key instead - ForbiddenComment:E2ETestKeyManagementService.kt$E2ETestKeyManagementService$// TODO: A full KeyManagementService implementation needs to record activity to the Audit Service and to limit ForbiddenComment:EncodingUtils.kt$// TODO: follow the crypto-conditions ASN.1 spec, some changes are needed to be compatible with the condition ForbiddenComment:Envelope.kt$Envelope$// TODO: don't recognise a type descriptor. ForbiddenComment:Envelope.kt$Envelope$// TODO: make the schema parsing lazy since mostly schemas will have been seen before and we only need it if we @@ -339,7 +336,6 @@ ForbiddenComment:IdentitySyncFlow.kt$IdentitySyncFlow.Send$// TODO: Can this be triggered automatically from [SendTransactionFlow]? ForbiddenComment:IdentitySyncFlow.kt$IdentitySyncFlow.Send$// TODO: Consider if this too restrictive - we perhaps should be checking the name on the signing certificate in the certificate path instead ForbiddenComment:InMemoryIdentityServiceTests.kt$InMemoryIdentityServiceTests$// TODO: Generate certificate with an EdDSA key rather than ECDSA - ForbiddenComment:InitialRegistrationCli.kt$InitialRegistration$// TODO: Move node identity generation logic from node to registration helper. ForbiddenComment:InteractiveShell.kt$// TODO: Add a command to view last N lines/tail/control log4j2 loggers. ForbiddenComment:InteractiveShell.kt$// TODO: Add command history. ForbiddenComment:InteractiveShell.kt$// TODO: Command completion. @@ -439,7 +435,6 @@ ForbiddenComment:SimmFlow.kt$SimmFlow.Requester$// TODO: The attachments need to be added somewhere ForbiddenComment:SimmFlow.kt$SimmFlow.Requester$// TODO: handle failures ForbiddenComment:SinglePartyNotaryService.kt$SinglePartyNotaryService$// TODO: Log the request here. Benchmarking shows that logging is expensive and we might get better performance - ForbiddenComment:StateMachineManagerUtils.kt$//TODO: instead of replacing the progress tracker after constructing the flow logic, we should inject it during fiber deserialization ForbiddenComment:Structures.kt$MoveCommand$// TODO: Replace Class here with a general contract constraints object ForbiddenComment:SwapData.kt$SwapData$// TODO: Fix below to be correct - change tenor and reference data ForbiddenComment:SwapDataView.kt$// TODO: Should be able to display an array ? @@ -1285,8 +1280,6 @@ SpreadOperator:HibernateQueryCriteriaParser.kt$AbstractQueryCriteriaParser$(*leftPredicates.toTypedArray(), *rightPredicates.toTypedArray()) SpreadOperator:HibernateQueryCriteriaParser.kt$AbstractQueryCriteriaParser$(*rightPredicates.toTypedArray()) SpreadOperator:HibernateQueryCriteriaParser.kt$HibernateAttachmentQueryCriteriaParser$(*predicateSet.toTypedArray()) - SpreadOperator:HibernateQueryCriteriaParser.kt$HibernateQueryCriteriaParser$(*combinedPredicates.toTypedArray()) - SpreadOperator:HibernateQueryCriteriaParser.kt$HibernateQueryCriteriaParser$(*joinPredicates.toTypedArray()) SpreadOperator:IRSDemo.kt$(*args) SpreadOperator:InstallShellExtensionsParser.kt$ShellExtensionsGenerator$(*commandAndArgs) SpreadOperator:InteractiveShell.kt$InteractiveShell$(clazz, *args) @@ -1387,6 +1380,7 @@ ThrowsCount:EnumTransforms.kt$EnumTransforms$ private fun validateNoCycles(constants: Map<String, Int>) ThrowsCount:JacksonSupport.kt$JacksonSupport.PartyDeserializer$private fun lookupByNameSegment(mapper: PartyObjectMapper, parser: JsonParser): Party ThrowsCount:JarScanningCordappLoader.kt$JarScanningCordappLoader$private fun parseVersion(versionStr: String?, attributeName: String): Int + ThrowsCount:KeyStoreHandler.kt$KeyStoreHandler$private fun getCertificateStores(): AllCertificateStores ThrowsCount:LedgerDSLInterpreter.kt$Verifies$ fun failsWith(expectedMessage: String?): EnforceVerifyOrFail ThrowsCount:MockServices.kt$ fun <T : SerializeAsToken> createMockCordaService(serviceHub: MockServices, serviceConstructor: (AppServiceHub) -> T): T ThrowsCount:NetworkRegistrationHelper.kt$NetworkRegistrationHelper$private fun validateCertificates( registeringPublicKey: PublicKey, registeringLegalName: CordaX500Name, expectedCertRole: CertRole, certificates: List<X509Certificate> ) @@ -1483,7 +1477,6 @@ TooGenericExceptionCaught:InternalUtils.kt$ex: Exception TooGenericExceptionCaught:InternalUtils.kt$th: Throwable TooGenericExceptionCaught:IssueCash.kt$IssueCash$e: Exception - TooGenericExceptionCaught:JVMAgentUtil.kt$JVMAgentUtil$e: Throwable TooGenericExceptionCaught:JacksonSupport.kt$JacksonSupport.PartyDeserializer$e: Exception TooGenericExceptionCaught:JacksonSupport.kt$JacksonSupport.PublicKeyDeserializer$e: Exception TooGenericExceptionCaught:JacksonSupport.kt$JacksonSupport.SecureHashDeserializer$e: Exception @@ -2005,7 +1998,6 @@ WildcardImport:DummyFungibleContract.kt$import net.corda.core.contracts.* WildcardImport:DummyLinearStateSchemaV1.kt$import javax.persistence.* WildcardImport:DummyLinearStateSchemaV2.kt$import javax.persistence.* - WildcardImport:E2ETestKeyManagementService.kt$import net.corda.core.crypto.* WildcardImport:EnumEvolvabilityTests.kt$import net.corda.core.serialization.* WildcardImport:EnumEvolvabilityTests.kt$import net.corda.serialization.internal.amqp.testutils.* WildcardImport:ErrorFlowTransition.kt$import net.corda.node.services.statemachine.* @@ -2154,7 +2146,6 @@ WildcardImport:NetworkMapTest.kt$import net.corda.core.internal.* WildcardImport:NetworkMapTest.kt$import net.corda.testing.core.* WildcardImport:NetworkMapTest.kt$import net.corda.testing.node.internal.* - WildcardImport:NetworkParametersReader.kt$import net.corda.core.internal.* WildcardImport:NetworkParametersReaderTest.kt$import net.corda.core.internal.* WildcardImport:NetworkParametersReaderTest.kt$import net.corda.nodeapi.internal.network.* WildcardImport:NetworkParametersTest.kt$import net.corda.testing.core.* 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 87fcc6961e..f106874b91 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -13,7 +13,6 @@ import net.corda.core.concurrent.CordaFuture import net.corda.core.context.InvocationContext import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.internal.AliasPrivateKey import net.corda.core.crypto.newSecureRandom import net.corda.core.flows.ContractUpgradeFlow import net.corda.core.flows.FinalityFlow @@ -147,15 +146,7 @@ import net.corda.node.utilities.NamedThreadFactory import net.corda.node.utilities.NotaryLoader import net.corda.nodeapi.internal.NodeInfoAndSigned import net.corda.nodeapi.internal.SignedNodeInfo -import net.corda.nodeapi.internal.config.CertificateStore import net.corda.nodeapi.internal.cordapp.CordappLoader -import net.corda.nodeapi.internal.crypto.CertificateType -import net.corda.nodeapi.internal.crypto.X509Utilities -import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_CA -import net.corda.nodeapi.internal.crypto.X509Utilities.DEFAULT_VALIDITY_WINDOW -import net.corda.nodeapi.internal.crypto.X509Utilities.DISTRIBUTED_NOTARY_COMPOSITE_KEY_ALIAS -import net.corda.nodeapi.internal.crypto.X509Utilities.DISTRIBUTED_NOTARY_KEY_ALIAS -import net.corda.nodeapi.internal.crypto.X509Utilities.NODE_IDENTITY_KEY_ALIAS import net.corda.nodeapi.internal.cryptoservice.CryptoService import net.corda.nodeapi.internal.cryptoservice.bouncycastle.BCCryptoService import net.corda.nodeapi.internal.lifecycle.NodeLifecycleEvent @@ -178,8 +169,6 @@ import org.jolokia.jvmagent.JolokiaServerConfig import org.slf4j.Logger import rx.Scheduler import java.lang.reflect.InvocationTargetException -import java.security.KeyPair -import java.security.cert.X509Certificate import java.sql.Connection import java.sql.Savepoint import java.time.Clock @@ -390,6 +379,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration, private val nodeLifecycleEventsDistributor = NodeLifecycleEventsDistributor().apply { add(checkpointDumper) } + protected val keyStoreHandler = KeyStoreHandler(configuration, cryptoService) + private fun T.tokenize(): T { tokenizableServices?.add(this as? SerializeAsToken ?: throw IllegalStateException("${this::class.java} is expected to be extending from SerializeAsToken")) @@ -434,17 +425,17 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } } + protected open fun initKeyStores() = keyStoreHandler.init() + open fun generateAndSaveNodeInfo(): NodeInfo { check(started == null) { "Node has already been started" } log.info("Generating nodeInfo ...") - val trustRoot = configuration.initKeyStores(cryptoService) + val trustRoot = initKeyStores() startDatabase() - val (identity, identityKeyPair) = obtainIdentity() - val nodeCa = configuration.signingCertificateStore.get()[CORDA_CLIENT_CA] - identityService.start(trustRoot, listOf(identity.certificate, nodeCa), pkToIdCache = pkToIdCache) + identityService.start(trustRoot, keyStoreHandler.nodeIdentity, pkToIdCache = pkToIdCache) return database.use { it.transaction { - val (_, nodeInfoAndSigned) = updateNodeInfo(identity, identityKeyPair, publish = false) + val nodeInfoAndSigned = updateNodeInfo(publish = false) nodeInfoAndSigned.nodeInfo } } @@ -488,7 +479,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, logVendorString(database, log) if (allowHibernateToManageAppSchema) { Node.printBasicNodeInfo("Initialising CorDapps to get schemas created by hibernate") - val trustRoot = configuration.initKeyStores(cryptoService) + val trustRoot = initKeyStores() networkMapClient?.start(trustRoot) val (netParams, signedNetParams) = NetworkParametersReader(trustRoot, networkMapClient, configuration.networkParametersPath).read() log.info("Loaded network parameters: $netParams") @@ -538,7 +529,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, nodeLifecycleEventsDistributor.distributeEvent(NodeLifecycleEvent.BeforeNodeStart(nodeServicesContext)) log.info("Node starting up ...") - val trustRoot = configuration.initKeyStores(cryptoService) + val trustRoot = initKeyStores() initialiseJolokia() schemaService.mappedSchemasWarnings().forEach { @@ -563,14 +554,11 @@ abstract class AbstractNode(val configuration: NodeConfiguration, networkMapCache.start(netParams.notaries) startDatabase() - val (identity, identityKeyPair) = obtainIdentity() - 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 }, pkToIdCache) + identityService.start(trustRoot, keyStoreHandler.nodeIdentity, netParams.notaries.map { it.identity }, pkToIdCache) - val (keyPairs, nodeInfoAndSigned, myNotaryIdentity) = database.transaction { - updateNodeInfo(identity, identityKeyPair, publish = true) + val nodeInfoAndSigned = database.transaction { + updateNodeInfo(publish = true) } val (nodeInfo, signedNodeInfo) = nodeInfoAndSigned @@ -598,7 +586,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, networkParametersHotloader) try { - startMessagingService(rpcOps, nodeInfo, myNotaryIdentity, netParams) + startMessagingService(rpcOps, nodeInfo, keyStoreHandler.notaryIdentity, netParams) } catch (e: Exception) { // Try to stop any started messaging services. stop() @@ -619,9 +607,9 @@ 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. - keyManagementService.start(keyPairs) + keyManagementService.start(keyStoreHandler.signingKeys.map { it.key to it.alias }) installCordaServices() - notaryService = maybeStartNotaryService(myNotaryIdentity) + notaryService = maybeStartNotaryService(keyStoreHandler.notaryIdentity) contractUpgradeService.start() vaultService.start() ScheduledActivityObserver.install(vaultService, schedulerService, flowLogicRefFactory) @@ -696,41 +684,15 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } } - private fun updateNodeInfo(identity: PartyAndCertificate, - identityKeyPair: KeyPair, - publish: Boolean): Triple, NodeInfoAndSigned, PartyAndCertificate?> { - val keyPairs = mutableSetOf(identityKeyPair) - - val myNotaryIdentity = configuration.notary?.let { - if (it.serviceLegalName != null) { - val (notaryIdentity, notaryIdentityKeyPair) = loadNotaryServiceIdentity(it.serviceLegalName) - keyPairs += notaryIdentityKeyPair - notaryIdentity - } else { - // The only case where the myNotaryIdentity will be the node's legal identity is for existing single notary services running - // an older version. Current single notary services (V4.6+) sign requests using a separate notary service identity so the - // notary identity will be different from the node's legal identity. - - // This check is here to ensure that a user does not accidentally/intentionally remove the serviceLegalName configuration - // parameter after a notary has been registered. If that was possible then notary would start and sign incoming requests - // with the node's legal identity key, corrupting the data. - check (!cryptoService.containsKey(DISTRIBUTED_NOTARY_KEY_ALIAS)) { - "The notary service key exists in the key store but no notary service legal name has been configured. " + - "Either include the relevant 'notary.serviceLegalName' configuration or validate this key is not necessary " + - "and remove from the key store." - } - identity - } - } - + private fun updateNodeInfo(publish: Boolean): NodeInfoAndSigned { val potentialNodeInfo = NodeInfo( myAddresses(), - setOf(identity, myNotaryIdentity).filterNotNull(), + setOf(keyStoreHandler.nodeIdentity, keyStoreHandler.notaryIdentity).filterNotNull(), versionInfo.platformVersion, serial = 0 ) - val nodeInfoFromDb = getPreviousNodeInfoIfPresent(identity) + val nodeInfoFromDb = getPreviousNodeInfoIfPresent(keyStoreHandler.nodeIdentity) val nodeInfo = if (potentialNodeInfo == nodeInfoFromDb?.copy(serial = 0)) { // The node info hasn't changed. We use the one from the database to preserve the serial. @@ -745,8 +707,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } val nodeInfoAndSigned = NodeInfoAndSigned(nodeInfo) { publicKey, serialised -> - val privateKey = keyPairs.single { it.public == publicKey }.private - DigitalSignature(cryptoService.sign((privateKey as AliasPrivateKey).alias, serialised.bytes)) + val alias = keyStoreHandler.signingKeys.single { it.key == publicKey }.alias + DigitalSignature(cryptoService.sign(alias, serialised.bytes)) } // Write the node-info file even if nothing's changed, just in case the file has been deleted. @@ -758,7 +720,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, tryPublishNodeInfoAsync(nodeInfoAndSigned.signed, networkMapClient) } - return Triple(keyPairs, nodeInfoAndSigned, myNotaryIdentity) + return nodeInfoAndSigned } private fun getPreviousNodeInfoIfPresent(identity: PartyAndCertificate): NodeInfo? { @@ -1084,127 +1046,6 @@ abstract class AbstractNode(val configuration: NodeConfiguration, myNotaryIdentity: PartyAndCertificate?, networkParameters: NetworkParameters) - /** - * Loads or generates the node's legal identity and key-pair. - * Note that obtainIdentity returns a KeyPair with an [AliasPrivateKey]. - */ - private fun obtainIdentity(): Pair { - val legalIdentityPrivateKeyAlias = "$NODE_IDENTITY_KEY_ALIAS" - - var signingCertificateStore = configuration.signingCertificateStore.get() - if (!cryptoService.containsKey(legalIdentityPrivateKeyAlias) && !signingCertificateStore.contains(legalIdentityPrivateKeyAlias)) { - // Directly use the X500 name to public key map, as the identity service requires the node identity to start correctly. - database.transaction { - val x500Map = PersistentIdentityService.createX500ToKeyMap(cacheFactory) - require(configuration.myLegalName !in x500Map) { - // There is already a party in the identity store for this node, but the key has been lost. If this node starts up, it will - // publish it's new key to the network map, which Corda cannot currently handle. To prevent this, stop the node from starting. - "Private key for the node legal identity not found (alias $legalIdentityPrivateKeyAlias) but the corresponding public key" + - " for it exists in the database. This suggests the identity for this node has been lost. Shutting down to prevent network map issues." - } - } - log.info("$legalIdentityPrivateKeyAlias not found in key store, generating fresh key!") - createAndStoreLegalIdentity(legalIdentityPrivateKeyAlias) - signingCertificateStore = configuration.signingCertificateStore.get() // We need to resync after [createAndStoreLegalIdentity]. - } else { - checkAliasMismatch(legalIdentityPrivateKeyAlias, signingCertificateStore) - } - val x509Cert = signingCertificateStore.query { getCertificate(legalIdentityPrivateKeyAlias) } - - // TODO: Use configuration to indicate composite key should be used instead of public key for the identity. - val certificates: List = signingCertificateStore.query { getCertificateChain(legalIdentityPrivateKeyAlias) } - check(certificates.first() == x509Cert) { - "Certificates from key store do not line up!" - } - - val subject = CordaX500Name.build(certificates.first().subjectX500Principal) - val legalName = configuration.myLegalName - if (subject != legalName) { - throw ConfigurationException("The configured legalName '$legalName' doesn't match what's in the key store: $subject") - } - - return getPartyAndCertificatePlusAliasKeyPair(certificates, legalIdentityPrivateKeyAlias) - } - - // Check if a key alias exists only in one of the cryptoService and certSigningStore. - private fun checkAliasMismatch(alias: String, certificateStore: CertificateStore) { - if (cryptoService.containsKey(alias) != certificateStore.contains(alias)) { - val keyExistsIn: String = if (cryptoService.containsKey(alias)) "CryptoService" else "signingCertificateStore" - throw IllegalStateException("CryptoService and signingCertificateStore are not aligned, the entry for key-alias: $alias is only found in $keyExistsIn") - } - } - - /** - * Loads notary service identity. In the case of the experimental RAFT and BFT notary clusters, this loads the pre-generated - * cluster identity that all worker nodes share. In the case of a simple single notary, this loads the notary service identity - * that is generated during initial registration and is used to sign notarisation requests. - * */ - private fun loadNotaryServiceIdentity(serviceLegalName: CordaX500Name): Pair { - val privateKeyAlias = "$DISTRIBUTED_NOTARY_KEY_ALIAS" - val compositeKeyAlias = "$DISTRIBUTED_NOTARY_COMPOSITE_KEY_ALIAS" - - val signingCertificateStore = configuration.signingCertificateStore.get() - val privateKeyAliasCertChain = try { - signingCertificateStore.query { getCertificateChain(privateKeyAlias) } - } catch (e: Exception) { - throw IllegalStateException("Certificate-chain for $privateKeyAlias cannot be found", e) - } - // A composite key is only required for BFT notaries. - val certificates = if (cryptoService.containsKey(compositeKeyAlias) && signingCertificateStore.contains(compositeKeyAlias)) { - val certificate = signingCertificateStore[compositeKeyAlias] - // We have to create the certificate chain for the composite key manually, this is because we don't have a keystore - // provider that understand compositeKey-privateKey combo. The cert chain is created using the composite key certificate + - // the tail of the private key certificates, as they are both signed by the same certificate chain. - listOf(certificate) + privateKeyAliasCertChain.drop(1) - } else { - checkAliasMismatch(compositeKeyAlias, signingCertificateStore) - // If [compositeKeyAlias] does not exist, we assume the notary is CFT, and each cluster member shares the same notary key pair. - privateKeyAliasCertChain - } - - val subject = CordaX500Name.build(certificates.first().subjectX500Principal) - if (subject != serviceLegalName) { - throw ConfigurationException("The name of the notary service '$serviceLegalName' doesn't " + - "match what's in the key store: $subject. You might need to adjust the configuration of `notary.serviceLegalName`.") - } - return getPartyAndCertificatePlusAliasKeyPair(certificates, privateKeyAlias) - } - - // Method to create a Pair, where KeyPair uses an AliasPrivateKey. - private fun getPartyAndCertificatePlusAliasKeyPair(certificates: List, privateKeyAlias: String): Pair { - val certPath = X509Utilities.buildCertPath(certificates) - val keyPair = KeyPair(cryptoService.getPublicKey(privateKeyAlias), AliasPrivateKey(privateKeyAlias)) - return Pair(PartyAndCertificate(certPath), keyPair) - } - - private fun createAndStoreLegalIdentity(alias: String): PartyAndCertificate { - val legalIdentityPublicKey = generateKeyPair(alias) - val signingCertificateStore = configuration.signingCertificateStore.get() - - val nodeCaCertPath = signingCertificateStore.value.getCertificateChain(X509Utilities.CORDA_CLIENT_CA) - val nodeCaCert = nodeCaCertPath[0] // This should be the same with signingCertificateStore[alias]. - - val identityCert = X509Utilities.createCertificate( - CertificateType.LEGAL_IDENTITY, - nodeCaCert.subjectX500Principal, - nodeCaCert.publicKey, - cryptoService.getSigner(X509Utilities.CORDA_CLIENT_CA), - nodeCaCert.subjectX500Principal, - legalIdentityPublicKey, - // TODO this might be smaller than DEFAULT_VALIDITY_WINDOW, shall we strictly apply DEFAULT_VALIDITY_WINDOW? - X509Utilities.getCertificateValidityWindow( - DEFAULT_VALIDITY_WINDOW.first, - DEFAULT_VALIDITY_WINDOW.second, - nodeCaCert) - ) - - val identityCertPath = listOf(identityCert) + nodeCaCertPath - signingCertificateStore.setCertPathOnly(alias, identityCertPath) - return PartyAndCertificate(X509Utilities.buildCertPath(identityCertPath)) - } - - protected open fun generateKeyPair(alias: String) = cryptoService.generateKeyPair(alias, cryptoService.defaultIdentitySignatureScheme()) - protected open fun makeVaultService(keyManagementService: KeyManagementService, services: ServicesForResolution, database: CordaPersistence, diff --git a/node/src/main/kotlin/net/corda/node/internal/KeyStoreHandler.kt b/node/src/main/kotlin/net/corda/node/internal/KeyStoreHandler.kt new file mode 100644 index 0000000000..a12bb74666 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/KeyStoreHandler.kt @@ -0,0 +1,230 @@ +package net.corda.node.internal + +import net.corda.core.crypto.toStringShort +import net.corda.core.identity.CordaX500Name +import net.corda.core.identity.PartyAndCertificate +import net.corda.core.internal.uncheckedCast +import net.corda.core.node.services.KeyManagementService +import net.corda.core.utilities.contextLogger +import net.corda.node.services.config.NodeConfiguration +import net.corda.node.services.config.configureWithDevSSLCertificate +import net.corda.nodeapi.internal.config.CertificateStore +import net.corda.nodeapi.internal.crypto.X509Utilities +import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_TLS +import net.corda.nodeapi.internal.crypto.X509Utilities.DISTRIBUTED_NOTARY_COMPOSITE_KEY_ALIAS +import net.corda.nodeapi.internal.crypto.X509Utilities.DISTRIBUTED_NOTARY_KEY_ALIAS +import net.corda.nodeapi.internal.crypto.X509Utilities.NODE_IDENTITY_KEY_ALIAS +import net.corda.nodeapi.internal.crypto.checkValidity +import net.corda.nodeapi.internal.cryptoservice.CryptoService +import net.corda.nodeapi.internal.cryptoservice.bouncycastle.BCCryptoService +import java.io.IOException +import java.math.BigInteger +import java.nio.file.NoSuchFileException +import java.security.GeneralSecurityException +import java.security.PublicKey +import java.security.cert.X509Certificate + +data class KeyAndAlias(val key: PublicKey, val alias: String) + +class KeyStoreHandler(private val configuration: NodeConfiguration, private val cryptoService: CryptoService) { + companion object { + private val log = contextLogger() + } + + private lateinit var _nodeIdentity: PartyAndCertificate + val nodeIdentity: PartyAndCertificate get() = _nodeIdentity + + private var _notaryIdentity: PartyAndCertificate? = null + val notaryIdentity: PartyAndCertificate? get() = _notaryIdentity + + private val _signingKeys: MutableSet = mutableSetOf() + val signingKeys: Set get() = _signingKeys.toSet() + + private lateinit var trustRoot: X509Certificate + + private lateinit var nodeKeyStore: CertificateStore + + /** + * Initialize key-stores and load identities. + * @param devModeKeyEntropy entropy for legal identity key derivation + * @return trust root certificate + */ + fun init(devModeKeyEntropy: BigInteger? = null): X509Certificate { + if (configuration.devMode) { + configuration.configureWithDevSSLCertificate(cryptoService, devModeKeyEntropy) + // configureWithDevSSLCertificate is a devMode process that writes directly to keystore files, so + // we should re-synchronise BCCryptoService with the updated keystore file. + if (cryptoService is BCCryptoService) { + cryptoService.resyncKeystore() + } + } + val certStores = getCertificateStores() + trustRoot = validateKeyStores(certStores) + nodeKeyStore = certStores.nodeKeyStore + loadIdentities() + return trustRoot + } + + private data class AllCertificateStores(val trustStore: CertificateStore, + val sslKeyStore: CertificateStore, + val nodeKeyStore: CertificateStore) + + private fun getCertificateStores(): AllCertificateStores { + try { + val sslKeyStore = configuration.p2pSslOptions.keyStore.get() + val nodeKeyStore = configuration.signingCertificateStore.get() + val trustStore = configuration.p2pSslOptions.trustStore.get() + return AllCertificateStores(trustStore, sslKeyStore, nodeKeyStore) + } catch (e: IOException) { + when { + e is NoSuchFileException -> throw IllegalArgumentException( + "One or more keyStores (identity or TLS) or trustStore not found. " + + "Please either copy your existing keys and certificates from another node, " + + "or if you don't have one yet, fill out the config file and run corda.jar initial-registration.", e) + e.cause is GeneralSecurityException -> throw IllegalArgumentException( + "At least one of the keystores or truststore passwords does not match configuration.", e) + else -> throw e + } + } + } + + private fun validateKeyStores(certStores: AllCertificateStores): X509Certificate { + // Check that trustStore contains the correct key-alias entry. + require(X509Utilities.CORDA_ROOT_CA in certStores.trustStore) { + "Alias for trustRoot key not found. Please ensure you have an updated trustStore file." + } + val trustRoot = certStores.trustStore[X509Utilities.CORDA_ROOT_CA] + + certStores.sslKeyStore.let { + val tlsKeyAlias = CORDA_CLIENT_TLS + + // Check that TLS keyStore contains the correct key-alias entry. + require(tlsKeyAlias in it) { + "Alias for TLS key not found. Please ensure you have an updated TLS keyStore file." + } + + // Check TLS certificate validity and print warning for expiry within next 30 days. + it[tlsKeyAlias].checkValidity({ + "TLS certificate for alias '$tlsKeyAlias' is expired." + }, { daysToExpiry -> + log.warn("TLS certificate for alias '$tlsKeyAlias' will expire in $daysToExpiry day(s).") + }) + + // Check TLS cert path chains to the trusted root. + val sslCertChainRoot = it.query { getCertificateChain(tlsKeyAlias) }.last() + require(sslCertChainRoot == trustRoot) { "TLS certificate must chain to the trusted root." } + } + + return trustRoot + } + + /** + * Loads node legal identity and notary service identity. + */ + private fun loadIdentities() { + val identityKeyAlias = NODE_IDENTITY_KEY_ALIAS + + _nodeIdentity = loadIdentity(identityKeyAlias, configuration.myLegalName) + _notaryIdentity = configuration.notary?.let { + loadNotaryIdentity(it.serviceLegalName, _nodeIdentity) + } + } + + /** + * Load key from CryptoService, so it can be used by KeyManagementService. + */ + private fun loadKeyFromCryptoService(alias: String) { + check(cryptoService.containsKey(alias)) { + "Key for node identity alias '$alias' not found in CryptoService." + } + val key = cryptoService.getPublicKey(alias)!! + log.info("Loaded node identity key: ${key.toStringShort()}, alias: $alias") + _signingKeys.add(KeyAndAlias(key, alias)) + } + + /** + * Loads the node's legal identity (or notary's service identity) certificate, public key and alias. + * + * If identity certificate has been renewed, the result will also contain previous public keys and aliases, + * so they can still be used by [KeyManagementService] for signing. + */ + private fun loadIdentity(alias: String, legalName: CordaX500Name): PartyAndCertificate { + require(alias in nodeKeyStore) { + "Alias '$alias' for node identity key is not in the keyStore file." + } + loadKeyFromCryptoService(alias) + + val certificate = nodeKeyStore.query { getCertificate(alias) } + val certificates = nodeKeyStore.query { getCertificateChain(alias) } + check(certificates.first() == certificate) { + "Certificates from key store do not line up!" + } + check(certificates.last() == trustRoot) { + "Certificate for node identity must chain to the trusted root." + } + + val subject = CordaX500Name.build(certificates.first().subjectX500Principal) + check(subject == legalName) { + "The configured legalName '$legalName' doesn't match what's in the key store: $subject" + } + + val identity = PartyAndCertificate(X509Utilities.buildCertPath(certificates)) + X509Utilities.validateCertPath(trustRoot, identity.certPath) + return identity + } + + /** + * Loads notary service identity. In the case of the experimental RAFT and BFT notary clusters, this loads the pre-generated + * cluster identity that all worker nodes share. In the case of a simple single notary, this loads the notary service identity + * that is generated during initial registration and is used to sign notarisation requests. + **/ + private fun loadNotaryIdentity(serviceLegalName: CordaX500Name?, nodeIdentity: PartyAndCertificate): PartyAndCertificate { + val notaryKeyAlias = DISTRIBUTED_NOTARY_KEY_ALIAS + val notaryCompositeKeyAlias = DISTRIBUTED_NOTARY_COMPOSITE_KEY_ALIAS + + if (serviceLegalName == null) { + // The only case where the notaryIdentity will be the node's legal identity is for existing single notary services running + // an older version. Current single notary services (V4.6+) sign requests using a separate notary service identity so the + // notary identity will be different from the node's legal identity. + + // This check is here to ensure that a user does not accidentally/intentionally remove the serviceLegalName configuration + // parameter after a notary has been registered. If that was possible then notary would start and sign incoming requests + // with the node's legal identity key, corrupting the data. + check(!nodeKeyStore.contains(notaryKeyAlias)) { + "The notary service key exists in the key store but no notary service legal name has been configured. " + + "Either include the relevant 'notary.serviceLegalName' configuration or validate this key is not necessary " + + "and remove from the key store." + } + return nodeIdentity + } + + // First load notary service identity that is generated during initial registration, then lookup for a composite identity. + // If alias for composite key does not exist, we assume the notary is CFT, and each cluster member shares the same notary key pair. + val serviceIdentity = loadIdentity(notaryKeyAlias, serviceLegalName) + if (notaryCompositeKeyAlias in nodeKeyStore) { + return loadCompositeIdentity(notaryCompositeKeyAlias, serviceLegalName, serviceIdentity) + } + return serviceIdentity + } + + /** + * Loads composite identity certificate for the provided [alias]. Composite identity can be stored as a certificate-only entry + * without associated signing key. Certificate chain is copied from the [baseIdentity]. + **/ + private fun loadCompositeIdentity(alias: String, legalName: CordaX500Name, baseIdentity: PartyAndCertificate): PartyAndCertificate { + val certificate = nodeKeyStore.query { getCertificate(alias) } + val subject = CordaX500Name.build(certificate.subjectX500Principal) + check(subject == legalName) { + "The configured legalName '$legalName' doesn't match what's in the key store: $subject" + } + + // We have to create the certificate chain for the composite key manually, this is because we don't have a keystore + // provider that understand compositeKey-privateKey combo. The cert chain is created using the composite key certificate + + // the tail of the private key certificates, as they are both signed by the same certificate chain. + val certificates: List = uncheckedCast(listOf(certificate) + baseIdentity.certPath.certificates.drop(1)) + + val identity = PartyAndCertificate(X509Utilities.buildCertPath(certificates)) + X509Utilities.validateCertPath(trustRoot, identity.certPath) + return identity + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/NodeKeyStoreUtilities.kt b/node/src/main/kotlin/net/corda/node/internal/NodeKeyStoreUtilities.kt deleted file mode 100644 index 904c0baa20..0000000000 --- a/node/src/main/kotlin/net/corda/node/internal/NodeKeyStoreUtilities.kt +++ /dev/null @@ -1,76 +0,0 @@ -package net.corda.node.internal - -import net.corda.core.utilities.loggerFor -import net.corda.node.services.config.NodeConfiguration -import net.corda.node.services.config.configureWithDevSSLCertificate -import net.corda.nodeapi.internal.config.CertificateStore -import net.corda.nodeapi.internal.crypto.X509Utilities -import net.corda.nodeapi.internal.cryptoservice.CryptoService -import net.corda.nodeapi.internal.cryptoservice.bouncycastle.BCCryptoService -import java.io.IOException -import java.security.KeyStoreException -import java.security.cert.X509Certificate - -private data class AllCertificateStores(val trustStore: CertificateStore, val sslKeyStore: CertificateStore, val identitiesKeyStore: CertificateStore) - - -internal fun NodeConfiguration.initKeyStores(cryptoService: CryptoService): X509Certificate { - if (devMode) { - configureWithDevSSLCertificate(cryptoService) - // configureWithDevSSLCertificate is a devMode process that writes directly to keystore files, so - // we should re-synchronise BCCryptoService with the updated keystore file. - if (cryptoService is BCCryptoService) { - cryptoService.resyncKeystore() - } - } - return validateKeyStores() -} - -private fun NodeConfiguration.validateKeyStores(): X509Certificate { - // Step 1. Check trustStore, sslKeyStore and identitiesKeyStore exist. - val certStores = try { - requireNotNull(getCertificateStores()) { - "One or more keyStores (identity or TLS) or trustStore not found. " + - "Please either copy your existing keys and certificates from another node, " + - "or if you don't have one yet, fill out the config file and run corda.jar initial-registration." - } - } catch (e: KeyStoreException) { - throw IllegalArgumentException("At least one of the keystores or truststore passwords does not match configuration.") - } - // Step 2. Check that trustStore contains the correct key-alias entry. - require(X509Utilities.CORDA_ROOT_CA in certStores.trustStore) { - "Alias for trustRoot key not found. Please ensure you have an updated trustStore file." - } - // Step 3. Check that tls keyStore contains the correct key-alias entry. - require(X509Utilities.CORDA_CLIENT_TLS in certStores.sslKeyStore) { - "Alias for TLS key not found. Please ensure you have an updated TLS keyStore file." - } - - // Step 4. Check that identity keyStores contain the correct key-alias entry for Node CA. - require(X509Utilities.CORDA_CLIENT_CA in certStores.identitiesKeyStore) { - "Alias for Node CA key not found. Please ensure you have an updated identity keyStore file." - } - - // Step 5. Check all cert paths chain to the trusted root. - val trustRoot = certStores.trustStore[X509Utilities.CORDA_ROOT_CA] - val sslCertChainRoot = certStores.sslKeyStore.query { getCertificateChain(X509Utilities.CORDA_CLIENT_TLS) }.last() - val nodeCaCertChainRoot = certStores.identitiesKeyStore.query { getCertificateChain(X509Utilities.CORDA_CLIENT_CA) }.last() - - require(sslCertChainRoot == trustRoot) { "TLS certificate must chain to the trusted root." } - require(nodeCaCertChainRoot == trustRoot) { "Client CA certificate must chain to the trusted root." } - - return trustRoot -} - -private fun NodeConfiguration.getCertificateStores(): AllCertificateStores? { - return try { - // The following will throw IOException if key file not found or KeyStoreException if keystore password is incorrect. - val sslKeyStore = p2pSslOptions.keyStore.get() - val signingCertificateStore = signingCertificateStore.get() - val trustStore = p2pSslOptions.trustStore.get() - AllCertificateStores(trustStore, sslKeyStore, signingCertificateStore) - } catch (e: IOException) { - loggerFor().error("IO exception while trying to validate keystores and truststore", e) - null - } -} diff --git a/node/src/main/kotlin/net/corda/node/internal/subcommands/InitialRegistrationCli.kt b/node/src/main/kotlin/net/corda/node/internal/subcommands/InitialRegistrationCli.kt index 9fb7df564a..c7e8cc12c5 100644 --- a/node/src/main/kotlin/net/corda/node/internal/subcommands/InitialRegistrationCli.kt +++ b/node/src/main/kotlin/net/corda/node/internal/subcommands/InitialRegistrationCli.kt @@ -76,10 +76,12 @@ class InitialRegistration(val baseDirectory: Path, private val networkRootTrustS HTTPNetworkRegistrationService( requireNotNull(conf.networkServices), versionInfo), - nodeRegistration).generateKeysAndRegister() + nodeRegistration).apply { + generateKeysAndRegister() + generateNodeIdentity() + } // Minimal changes to make registration tool create node identity. - // TODO: Move node identity generation logic from node to registration helper. val node = startup.createNode(conf, versionInfo) if(!skipSchemaMigration) { node.runDatabaseMigrationScripts(updateCoreSchemas = true, updateAppSchemas = true, updateAppSchemasWithCheckpoints = false) diff --git a/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt b/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt index d4e0239537..524c565579 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt @@ -5,6 +5,7 @@ import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigParseOptions import net.corda.cliutils.CordaSystemUtils import net.corda.common.configuration.parsing.internal.Configuration +import net.corda.core.crypto.Crypto import net.corda.core.identity.CordaX500Name import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.createDirectories @@ -17,12 +18,15 @@ import net.corda.nodeapi.internal.config.FileBasedCertificateStoreSupplier import net.corda.nodeapi.internal.config.MutualSslConfiguration import net.corda.nodeapi.internal.config.toProperties import net.corda.nodeapi.internal.crypto.X509KeyStore +import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.cryptoservice.CryptoService import net.corda.nodeapi.internal.cryptoservice.bouncycastle.BCCryptoService import net.corda.nodeapi.internal.installDevNodeCaCertPath import net.corda.nodeapi.internal.loadDevCaTrustStore import net.corda.nodeapi.internal.registerDevP2pCertificates +import net.corda.nodeapi.internal.storeLegalIdentity import org.slf4j.LoggerFactory +import java.math.BigInteger import java.nio.file.Path import kotlin.math.min @@ -165,10 +169,16 @@ object ConfigHelper { * the CA certs in Node resources. Then provision KeyStores into certificates folder under node path. */ // TODO Move this to KeyStoreConfigHelpers. -fun NodeConfiguration.configureWithDevSSLCertificate(cryptoService: CryptoService? = null) = p2pSslOptions.configureDevKeyAndTrustStores(myLegalName, signingCertificateStore, certificatesDirectory, cryptoService) +fun NodeConfiguration.configureWithDevSSLCertificate(cryptoService: CryptoService? = null, entropy: BigInteger? = null) = + p2pSslOptions.configureDevKeyAndTrustStores(myLegalName, signingCertificateStore, certificatesDirectory, cryptoService, entropy) // TODO Move this to KeyStoreConfigHelpers. -fun MutualSslConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500Name, signingCertificateStore: FileBasedCertificateStoreSupplier, certificatesDirectory: Path, cryptoService: CryptoService? = null) { +@Suppress("ComplexMethod") +fun MutualSslConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500Name, + signingCertificateStore: FileBasedCertificateStoreSupplier, + certificatesDirectory: Path, + cryptoService: CryptoService? = null, + entropy: BigInteger? = null) { val specifiedTrustStore = trustStore.getOptional() val specifiedKeyStore = keyStore.getOptional() @@ -187,7 +197,15 @@ fun MutualSslConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500N when (cryptoService) { is BCCryptoService, null -> { val signingKeyStore = FileBasedCertificateStoreSupplier(signingCertificateStore.path, signingCertificateStore.storePassword, signingCertificateStore.entryPassword).get(true) - .also { it.installDevNodeCaCertPath(myLegalName) } + .also { + it.installDevNodeCaCertPath(myLegalName) + val keyPair = if (entropy != null) { + Crypto.deriveKeyPairFromEntropy(Crypto.DEFAULT_SIGNATURE_SCHEME, entropy) + } else { + Crypto.generateKeyPair() + } + it.storeLegalIdentity(X509Utilities.NODE_IDENTITY_KEY_ALIAS, keyPair) + } // Move distributed service composite key (generated by IdentityGenerator.generateToDisk) to keystore if exists. val distributedServiceKeystore = certificatesDirectory / "distributedService.jks" 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 d9d8906861..947b6c22bb 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 @@ -215,13 +215,15 @@ class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSeri fun start( trustRoot: X509Certificate, - caCertificates: List = emptyList(), + ourIdentity: PartyAndCertificate, notaryIdentities: List = emptyList(), pkToIdCache: WritablePublicKeyToOwningIdentityCache ) { _trustRoot = trustRoot _trustAnchor = TrustAnchor(trustRoot, null) - _caCertStore = CertStore.getInstance("Collection", CollectionCertStoreParameters(caCertificates.toSet() + trustRoot)) + // Extract Node CA certificate from node identity certificate path + val certificates = setOf(ourIdentity.certificate, ourIdentity.certPath.certificates[1], trustRoot) + _caCertStore = CertStore.getInstance("Collection", CollectionCertStoreParameters(certificates)) _pkToIdCache = pkToIdCache notaryIdentityCache.addAll(notaryIdentities) } 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 84a274e4ac..13cfbf9bf1 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 @@ -1,7 +1,6 @@ package net.corda.node.services.keys import net.corda.core.crypto.* -import net.corda.core.crypto.internal.AliasPrivateKey import net.corda.core.internal.NamedCacheFactory import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.serialize @@ -21,7 +20,7 @@ import javax.persistence.* import kotlin.collections.LinkedHashSet /** - * A persistent re-implementation of [E2ETestKeyManagementService] to support CryptoService for initial keys and + * A persistent implementation of [KeyManagementServiceInternal] to support CryptoService for initial keys and * database storage for anonymous fresh keys. * * This is not the long-term implementation. See the list of items in the above class. @@ -74,10 +73,9 @@ class BasicHSMKeyManagementService( // A map for anonymous keys. private val keysMap = createKeyMap(cacheFactory) - override fun start(initialKeyPairs: Set) { - initialKeyPairs.forEach { - require(it.private is AliasPrivateKey) { "${this.javaClass.name} supports AliasPrivateKeys only, but ${it.private.algorithm} key was found" } - originalKeysMap[Crypto.toSupportedPublicKey(it.public)] = (it.private as AliasPrivateKey).alias + override fun start(initialKeysAndAliases: Iterable>) { + initialKeysAndAliases.forEach { + originalKeysMap[Crypto.toSupportedPublicKey(it.first)] = it.second } } 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 deleted file mode 100644 index a850d82d5c..0000000000 --- a/node/src/main/kotlin/net/corda/node/services/keys/E2ETestKeyManagementService.kt +++ /dev/null @@ -1,91 +0,0 @@ -package net.corda.node.services.keys - -import net.corda.core.crypto.* -import net.corda.core.crypto.internal.AliasPrivateKey -import net.corda.core.internal.ThreadBox -import net.corda.core.node.services.IdentityService -import net.corda.core.serialization.SingletonSerializeAsToken -import net.corda.nodeapi.internal.cryptoservice.CryptoService -import net.corda.nodeapi.internal.cryptoservice.bouncycastle.BCCryptoService -import org.bouncycastle.operator.ContentSigner -import java.security.KeyPair -import java.security.PrivateKey -import java.security.PublicKey -import java.util.* -import javax.annotation.concurrent.ThreadSafe - -/** - * A simple in-memory KMS that doesn't bother saving keys to disk. A real implementation would: - * - * - Probably be accessed via the network layer as an internal node service i.e. via a message queue, so it can run - * on a separate/firewalled service. - * - Use the flow framework so requests to fetch keys can be suspended whilst a human signs off on the request. - * - Use deterministic key derivation. - * - Possibly have some sort of TREZOR-like two-factor authentication ability. - * - * etc. - */ -@ThreadSafe -class E2ETestKeyManagementService(override val identityService: IdentityService, private val cryptoService: CryptoService? = null) : SingletonSerializeAsToken(), KeyManagementServiceInternal { - - private class InnerState { - val keys = HashMap() - } - - private val mutex = ThreadBox(InnerState()) - // Accessing this map clones it. - override val keys: Set get() = mutex.locked { keys.keys } - // Maintain a map from PublicKey to alias for the initial keys. - - val keyPairs: Set get() = mutex.locked { keys.map { KeyPair(it.key, it.value) }.toSet() } - - override fun start(initialKeyPairs: Set) { - mutex.locked { - for (key in initialKeyPairs) { - var privateKey = key.private - if (privateKey is AliasPrivateKey && cryptoService is BCCryptoService) { - privateKey = cryptoService.certificateStore.query { - getPrivateKey((privateKey as AliasPrivateKey).alias, cryptoService.certificateStore.entryPassword) - } - } - keys[key.public] = privateKey - } - } - } - - override fun freshKeyInternal(externalId: UUID?): PublicKey { - if (externalId != null) { - throw UnsupportedOperationException("This operation is only supported by persistent key management service variants.") - } - val keyPair = generateKeyPair() - mutex.locked { - keys[keyPair.public] = keyPair.private - } - return keyPair.public - } - - override fun getSigner(publicKey: PublicKey): ContentSigner = getSigner(getSigningKeyPair(publicKey)) - - private fun getSigningKeyPair(publicKey: PublicKey): KeyPair { - return mutex.locked { - val pk = publicKey.keys.first { keys.containsKey(it) } - KeyPair(pk, keys[pk]!!) - } - } - - override fun filterMyKeys(candidateKeys: Iterable): Iterable { - return mutex.locked { candidateKeys.filter { it in this.keys } } - } - - 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/keys/KeyManagementServiceInternal.kt b/node/src/main/kotlin/net/corda/node/services/keys/KeyManagementServiceInternal.kt index 5f799b359b..a8ab6bc676 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 @@ -4,7 +4,6 @@ import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.services.IdentityService import net.corda.core.node.services.KeyManagementService import org.bouncycastle.operator.ContentSigner -import java.security.KeyPair import java.security.PublicKey import java.util.* @@ -12,7 +11,7 @@ interface KeyManagementServiceInternal : KeyManagementService { val identityService: IdentityService - fun start(initialKeyPairs: Set) + fun start(initialKeysAndAliases: Iterable>) fun freshKeyInternal(externalId: UUID?): PublicKey diff --git a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt index 8d2558ca8e..fd77518644 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt @@ -17,6 +17,7 @@ import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_CA import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_TLS import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA import net.corda.nodeapi.internal.crypto.X509Utilities.DEFAULT_VALIDITY_WINDOW +import net.corda.nodeapi.internal.crypto.x509 import net.corda.nodeapi.internal.cryptoservice.CryptoService import net.corda.nodeapi.internal.cryptoservice.bouncycastle.BCCryptoService import org.bouncycastle.asn1.x500.X500Name @@ -155,6 +156,50 @@ open class NetworkRegistrationHelper( requestIdStore.deleteIfExists() } + fun generateNodeIdentity() { + certificatesDirectory.safeSymbolicRead().createDirectories() + // We need this in case cryptoService and certificateStore share the same KeyStore (for backwards compatibility purposes). + // If we didn't, then an update to cryptoService wouldn't be reflected to certificateStore that is already loaded in memory. + val certStore: CertificateStore = if (cryptoService is BCCryptoService) cryptoService.certificateStore else certificateStore + + if (!certStore.contains(nodeCaKeyAlias)) { + logProgress("Node CA key doesn't exist, program will now terminate...") + throw IllegalStateException("Node CA not found") + } + + val nodeIdentityAlias = X509Utilities.NODE_IDENTITY_KEY_ALIAS + if (certStore.contains(nodeIdentityAlias)) { + logProgress("Node identity already exists, Corda node will now terminate...") + return + } + + certStore.update { + logProgress("Generating node identity certificate.") + val nodeIdentityPublicKey = cryptoService.generateKeyPair(nodeIdentityAlias, cryptoService.defaultIdentitySignatureScheme()) + val nodeCaCertChain = getCertificateChain(nodeCaKeyAlias) + val nodeCaCertificate = nodeCaCertChain.first() + val validityWindow = X509Utilities.getCertificateValidityWindow(DEFAULT_VALIDITY_WINDOW.first, DEFAULT_VALIDITY_WINDOW.second, nodeCaCertificate) + + val nodeIdentityCert = X509Utilities.createCertificate( + CertificateType.LEGAL_IDENTITY, + nodeCaCertificate.subjectX500Principal, + nodeCaCertificate.x509.publicKey, + cryptoService.getSigner(nodeCaKeyAlias), + nodeCaCertificate.subjectX500Principal, + nodeIdentityPublicKey, + validityWindow, + crlDistPoint = null, + crlIssuer = null) + + logger.info("Generated Node Identity certificate: $nodeIdentityCert") + + val nodeIdentityCertificateChain: List = listOf(nodeIdentityCert) + nodeCaCertChain + X509Utilities.validateCertificateChain(rootCert, nodeIdentityCertificateChain) + certStore.setCertPathOnly(nodeIdentityAlias, nodeIdentityCertificateChain) + } + logProgress("Node identity private key and certificate chain stored in $nodeIdentityAlias.") + } + private fun loadOrGenerateKeyPair(keyAlias: String): PublicKey { return if (cryptoService.containsKey(keyAlias)) { cryptoService.getPublicKey(keyAlias)!! diff --git a/node/src/test/kotlin/net/corda/node/internal/KeyStoreHandlerTest.kt b/node/src/test/kotlin/net/corda/node/internal/KeyStoreHandlerTest.kt new file mode 100644 index 0000000000..a2f0dd3d6a --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/internal/KeyStoreHandlerTest.kt @@ -0,0 +1,392 @@ +package net.corda.node.internal + +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.whenever +import net.corda.core.crypto.CompositeKey +import net.corda.core.crypto.Crypto +import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.div +import net.corda.coretesting.internal.rigorousMock +import net.corda.coretesting.internal.stubs.CertificateStoreStubs +import net.corda.node.services.config.NodeConfiguration +import net.corda.node.services.config.NotaryConfig +import net.corda.node.services.config.configureDevKeyAndTrustStores +import net.corda.nodeapi.internal.DEV_CA_KEY_STORE_PASS +import net.corda.nodeapi.internal.DEV_INTERMEDIATE_CA +import net.corda.nodeapi.internal.DEV_ROOT_CA +import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair +import net.corda.nodeapi.internal.crypto.CertificateType +import net.corda.nodeapi.internal.crypto.X509Utilities +import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_CA +import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_TLS +import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA +import net.corda.nodeapi.internal.crypto.X509Utilities.DISTRIBUTED_NOTARY_COMPOSITE_KEY_ALIAS +import net.corda.nodeapi.internal.crypto.X509Utilities.DISTRIBUTED_NOTARY_KEY_ALIAS +import net.corda.nodeapi.internal.crypto.X509Utilities.NODE_IDENTITY_KEY_ALIAS +import net.corda.nodeapi.internal.cryptoservice.CryptoService +import net.corda.nodeapi.internal.cryptoservice.bouncycastle.BCCryptoService +import net.corda.testing.core.ALICE_NAME +import net.corda.testing.core.BOB_NAME +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.security.KeyPair +import java.security.PublicKey + +class KeyStoreHandlerTest { + @Rule + @JvmField + val tempFolder = TemporaryFolder() + + private val certificateDir get() = tempFolder.root.toPath() / "certificates" + + private val config = rigorousMock() + + private val keyStore get() = config.signingCertificateStore.get() + + private lateinit var cryptoService: BCCryptoService + + private lateinit var keyStoreHandler: KeyStoreHandler + + @Before + fun before() { + val signingCertificateStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificateDir) + val p2pSslOptions = CertificateStoreStubs.P2P.withCertificatesDirectory(certificateDir) + p2pSslOptions.configureDevKeyAndTrustStores(ALICE_NAME, signingCertificateStore, certificateDir) + + config.also { + doReturn(false).whenever(it).devMode + doReturn(signingCertificateStore).whenever(it).signingCertificateStore + doReturn(p2pSslOptions).whenever(it).p2pSslOptions + doReturn(ALICE_NAME).whenever(it).myLegalName + doReturn(null).whenever(it).notary + } + cryptoService = BCCryptoService(ALICE_NAME.x500Principal, signingCertificateStore) + keyStoreHandler = KeyStoreHandler(config, cryptoService) + } + + @Test(timeout = 300_000) + fun `missing node keystore`() { + val signingCertificateStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificateDir, + certificateStoreFileName = "invalid.jks") + doReturn(signingCertificateStore).whenever(config).signingCertificateStore + + assertThatThrownBy { + keyStoreHandler.init() + }.hasMessageContaining("One or more keyStores (identity or TLS) or trustStore not found.") + } + + @Test(timeout = 300_000) + fun `missing truststore`() { + val p2pSslOptions = CertificateStoreStubs.P2P.withCertificatesDirectory(certificateDir, trustStoreFileName = "invalid.jks") + doReturn(p2pSslOptions).whenever(config).p2pSslOptions + + assertThatThrownBy { + keyStoreHandler.init() + }.hasMessageContaining("One or more keyStores (identity or TLS) or trustStore not found.") + } + + @Test(timeout = 300_000) + fun `missing TLS keystore`() { + val p2pSslOptions = CertificateStoreStubs.P2P.withCertificatesDirectory(certificateDir, keyStoreFileName = "invalid.jks") + doReturn(p2pSslOptions).whenever(config).p2pSslOptions + + assertThatThrownBy { + keyStoreHandler.init() + }.hasMessageContaining("One or more keyStores (identity or TLS) or trustStore not found.") + } + + @Test(timeout = 300_000) + fun `invalid node keystore password`() { + val signingCertificateStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificateDir, password = "invalid") + doReturn(signingCertificateStore).whenever(config).signingCertificateStore + + assertThatThrownBy { + keyStoreHandler.init() + }.hasMessageContaining("At least one of the keystores or truststore passwords does not match configuration") + } + + @Test(timeout = 300_000) + fun `invalid truststore password`() { + val p2pSslOptions = CertificateStoreStubs.P2P.withCertificatesDirectory(certificateDir, trustStorePassword = "invalid") + doReturn(p2pSslOptions).whenever(config).p2pSslOptions + + assertThatThrownBy { + keyStoreHandler.init() + }.hasMessageContaining("At least one of the keystores or truststore passwords does not match configuration") + } + + @Test(timeout = 300_000) + fun `invalid TLS keystore password`() { + val p2pSslOptions = CertificateStoreStubs.P2P.withCertificatesDirectory(certificateDir, keyStorePassword = "invalid") + doReturn(p2pSslOptions).whenever(config).p2pSslOptions + + assertThatThrownBy { + keyStoreHandler.init() + }.hasMessageContaining("At least one of the keystores or truststore passwords does not match configuration") + } + + @Test(timeout = 300_000) + fun `missing trusted root in a truststore`() { + config.p2pSslOptions.trustStore.get().update { + internal.deleteEntry(CORDA_ROOT_CA) + } + + assertThatThrownBy { + keyStoreHandler.init() + }.hasMessageContaining("Alias for trustRoot key not found. Please ensure you have an updated trustStore file") + } + + @Test(timeout = 300_000) + fun `missing TLS alias`() { + config.p2pSslOptions.keyStore.get().update { + internal.deleteEntry(CORDA_CLIENT_TLS) + } + + assertThatThrownBy { + keyStoreHandler.init() + }.hasMessageContaining("Alias for TLS key not found. Please ensure you have an updated TLS keyStore file") + } + + @Test(timeout = 300_000) + fun `load TLS certificate with untrusted root`() { + val keyPair = Crypto.generateKeyPair() + val tlsKeyPair = Crypto.generateKeyPair() + val untrustedRoot = X509Utilities.createSelfSignedCACertificate(ALICE_NAME.x500Principal, keyPair) + val tlsCert = X509Utilities.createCertificate(CertificateType.TLS, untrustedRoot, keyPair, ALICE_NAME.x500Principal, + tlsKeyPair.public) + + config.p2pSslOptions.keyStore.get().update { + setPrivateKey(CORDA_CLIENT_TLS, tlsKeyPair.private, listOf(tlsCert, untrustedRoot), config.p2pSslOptions.keyStore.entryPassword) + } + + assertThatThrownBy { + keyStoreHandler.init() + }.hasMessageContaining("TLS certificate must chain to the trusted root") + } + + @Test(timeout = 300_000) + fun `valid trust root is returned`() { + val expectedRoot = config.p2pSslOptions.trustStore.get()[CORDA_ROOT_CA] + val actualRoot = keyStoreHandler.init() + + assertThat(actualRoot).isEqualTo(expectedRoot) + } + + @Test(timeout = 300_000) + fun `keystore creation in dev mode`() { + val devCertificateDir = tempFolder.root.toPath() / "certificates-dev" + val signingCertificateStore = CertificateStoreStubs.Signing.withCertificatesDirectory(devCertificateDir) + val p2pSslOptions = CertificateStoreStubs.P2P.withCertificatesDirectory(devCertificateDir) + val devCryptoService = BCCryptoService(config.myLegalName.x500Principal, signingCertificateStore) + + doReturn(true).whenever(config).devMode + doReturn(signingCertificateStore).whenever(config).signingCertificateStore + doReturn(p2pSslOptions).whenever(config).p2pSslOptions + doReturn(devCertificateDir).whenever(config).certificatesDirectory + + assertThat(devCryptoService.containsKey(NODE_IDENTITY_KEY_ALIAS)).isFalse() + + KeyStoreHandler(config, devCryptoService).init() + + assertThat(config.p2pSslOptions.trustStore.get().contains(CORDA_ROOT_CA)).isTrue() + assertThat(config.p2pSslOptions.keyStore.get().contains(CORDA_CLIENT_TLS)).isTrue() + assertThat(config.signingCertificateStore.get().contains(NODE_IDENTITY_KEY_ALIAS)).isTrue() + assertThat(devCryptoService.containsKey(NODE_IDENTITY_KEY_ALIAS)).isTrue() + } + + @Test(timeout = 300_000) + fun `load node identity`() { + keyStoreHandler.init() + + val certificate = keyStore[NODE_IDENTITY_KEY_ALIAS] + assertThat(keyStoreHandler.nodeIdentity.certificate).isEqualTo(certificate) + assertThat(keyStoreHandler.notaryIdentity).isNull() + assertThat(keyStoreHandler.signingKeys).containsExactly(KeyAndAlias(certificate.publicKey, NODE_IDENTITY_KEY_ALIAS)) + } + + @Test(timeout = 300_000) + fun `load node identity without node CA`() { + assertThat(keyStore[CORDA_CLIENT_CA]).isNotNull + keyStore.update { internal.deleteEntry(CORDA_CLIENT_CA) } + + keyStoreHandler.init() + + val certificate = keyStore[NODE_IDENTITY_KEY_ALIAS] + assertThat(keyStoreHandler.nodeIdentity.certificate).isEqualTo(certificate) + assertThat(keyStoreHandler.notaryIdentity).isNull() + assertThat(keyStoreHandler.signingKeys).containsExactly(KeyAndAlias(certificate.publicKey, NODE_IDENTITY_KEY_ALIAS)) + } + + @Test(timeout = 300_000) + fun `load node identity with missing alias`() { + keyStore.update { internal.deleteEntry(NODE_IDENTITY_KEY_ALIAS) } + + assertThatThrownBy { + keyStoreHandler.init() + }.hasMessageContaining("node identity key is not in the keyStore file") + } + + @Test(timeout = 300_000) + fun `load node identity with missing key in CryptoService`() { + val cryptoServiceMock = rigorousMock() + doReturn(false).whenever(cryptoServiceMock).containsKey(NODE_IDENTITY_KEY_ALIAS) + keyStoreHandler = KeyStoreHandler(config, cryptoServiceMock) + + assertThatThrownBy { + keyStoreHandler.init() + }.hasMessageContaining("Key for node identity alias '$NODE_IDENTITY_KEY_ALIAS' not found in CryptoService") + } + + @Test(timeout = 300_000) + fun `load node identity with untrusted root`() { + val untrustedRoot = X509Utilities.createSelfSignedCACertificate(ALICE_NAME.x500Principal, Crypto.generateKeyPair()) + + keyStore.update { + val privateKey = getPrivateKey(NODE_IDENTITY_KEY_ALIAS, DEV_CA_KEY_STORE_PASS) + val certificates = getCertificateChain(NODE_IDENTITY_KEY_ALIAS) + setPrivateKey(NODE_IDENTITY_KEY_ALIAS, privateKey, certificates.dropLast(1) + untrustedRoot, DEV_CA_KEY_STORE_PASS) + } + + assertThatThrownBy { + keyStoreHandler.init() + }.hasMessageContaining("Certificate for node identity must chain to the trusted root") + } + + @Test(timeout = 300_000) + fun `load node identity with wrong legal name`() { + doReturn(BOB_NAME).whenever(config).myLegalName + + assertThatThrownBy { + keyStoreHandler.init() + }.hasMessageContaining("The configured legalName").hasMessageContaining("doesn't match what's in the key store") + } + + @Test(timeout = 300_000) + fun `load node identity with wrong certificate path`() { + keyStore.update { + val privateKey = getPrivateKey(NODE_IDENTITY_KEY_ALIAS, DEV_CA_KEY_STORE_PASS) + val certificates = getCertificateChain(NODE_IDENTITY_KEY_ALIAS) + setPrivateKey(NODE_IDENTITY_KEY_ALIAS, privateKey, certificates.take(1) + certificates.drop(2), DEV_CA_KEY_STORE_PASS) + } + + assertThatThrownBy { + keyStoreHandler.init() + }.hasMessageContaining("Cert path failed to validate") + } + + @Test(timeout = 300_000) + fun `load old style notary identity`() { + val notaryConfig = rigorousMock() + doReturn(null).whenever(notaryConfig).serviceLegalName + doReturn(notaryConfig).whenever(config).notary + + keyStoreHandler.init() + + val certificate = keyStore[NODE_IDENTITY_KEY_ALIAS] + assertThat(keyStoreHandler.nodeIdentity.certificate).isEqualTo(certificate) + assertThat(keyStoreHandler.notaryIdentity).isNotNull + assertThat(keyStoreHandler.notaryIdentity!!.certificate).isEqualTo(certificate) + assertThat(keyStoreHandler.signingKeys).containsExactly(KeyAndAlias(certificate.publicKey, NODE_IDENTITY_KEY_ALIAS)) + } + + private fun createNotaryCertificate(publicKey: PublicKey, name: CordaX500Name) = X509Utilities.createCertificate( + CertificateType.SERVICE_IDENTITY, + DEV_INTERMEDIATE_CA.certificate, + DEV_INTERMEDIATE_CA.keyPair, + name.x500Principal, + publicKey) + + private fun generateIdentity(alias: String, name: CordaX500Name, type: CertificateType, parentAlias: String? = null): PublicKey { + val keyPair = Crypto.generateKeyPair() + val (parent, chain) = if (parentAlias != null) { + keyStore.query { + val parentCert = getCertificate(parentAlias) + val parentKey = getPrivateKey(parentAlias, DEV_CA_KEY_STORE_PASS) + CertificateAndKeyPair(parentCert, KeyPair(parentCert.publicKey, parentKey)) to getCertificateChain(parentAlias) + } + } else { + DEV_INTERMEDIATE_CA to listOf(DEV_INTERMEDIATE_CA.certificate, DEV_ROOT_CA.certificate) + } + val certificate = X509Utilities.createCertificate(type, parent.certificate, parent.keyPair, name.x500Principal, keyPair.public) + keyStore.update { + setPrivateKey(alias, keyPair.private, listOf(certificate) + chain, DEV_CA_KEY_STORE_PASS) + } + cryptoService.resyncKeystore() + return keyPair.public + } + + @Test(timeout = 300_000) + fun `load notary identity`() { + val notaryConfig = rigorousMock() + doReturn(BOB_NAME).whenever(notaryConfig).serviceLegalName + doReturn(notaryConfig).whenever(config).notary + + generateIdentity(DISTRIBUTED_NOTARY_KEY_ALIAS, BOB_NAME, CertificateType.SERVICE_IDENTITY) + + keyStoreHandler.init() + + val nodeCert = keyStore[NODE_IDENTITY_KEY_ALIAS] + val notaryCert = keyStore[DISTRIBUTED_NOTARY_KEY_ALIAS] + assertThat(keyStoreHandler.nodeIdentity.certificate).isEqualTo(nodeCert) + assertThat(keyStoreHandler.notaryIdentity).isNotNull + assertThat(keyStoreHandler.notaryIdentity!!.certificate).isEqualTo(notaryCert) + assertThat(keyStoreHandler.signingKeys).containsExactly( + KeyAndAlias(nodeCert.publicKey, NODE_IDENTITY_KEY_ALIAS), + KeyAndAlias(notaryCert.publicKey, DISTRIBUTED_NOTARY_KEY_ALIAS) + ) + } + + @Test(timeout = 300_000) + fun `load notary identity with wrong legal name`() { + val notaryConfig = rigorousMock() + doReturn(BOB_NAME).whenever(notaryConfig).serviceLegalName + doReturn(notaryConfig).whenever(config).notary + + generateIdentity(DISTRIBUTED_NOTARY_KEY_ALIAS, ALICE_NAME, CertificateType.SERVICE_IDENTITY) + + assertThatThrownBy { + keyStoreHandler.init() + }.hasMessageContaining("The configured legalName").hasMessageContaining("doesn't match what's in the key store") + } + + @Test(timeout = 300_000) + fun `load notary composite identity`() { + val notaryConfig = rigorousMock() + doReturn(BOB_NAME).whenever(notaryConfig).serviceLegalName + doReturn(notaryConfig).whenever(config).notary + + val notaryKey = generateIdentity(DISTRIBUTED_NOTARY_KEY_ALIAS, BOB_NAME, CertificateType.SERVICE_IDENTITY) + val compositeKey = CompositeKey.Builder().addKey(notaryKey).build() + keyStore[DISTRIBUTED_NOTARY_COMPOSITE_KEY_ALIAS] = createNotaryCertificate(compositeKey, BOB_NAME) + + keyStoreHandler.init() + + val nodeCert = keyStore[NODE_IDENTITY_KEY_ALIAS] + assertThat(keyStoreHandler.nodeIdentity.certificate).isEqualTo(nodeCert) + assertThat(keyStoreHandler.notaryIdentity).isNotNull + assertThat(keyStoreHandler.notaryIdentity!!.certificate).isEqualTo(keyStore[DISTRIBUTED_NOTARY_COMPOSITE_KEY_ALIAS]) + assertThat(keyStoreHandler.signingKeys).containsExactly( + KeyAndAlias(nodeCert.publicKey, NODE_IDENTITY_KEY_ALIAS), + KeyAndAlias(notaryKey, DISTRIBUTED_NOTARY_KEY_ALIAS) + ) + } + + @Test(timeout = 300_000) + fun `load notary composite identity with wrong legal name`() { + val notaryConfig = rigorousMock() + doReturn(BOB_NAME).whenever(notaryConfig).serviceLegalName + doReturn(notaryConfig).whenever(config).notary + + val notaryKey = generateIdentity(DISTRIBUTED_NOTARY_KEY_ALIAS, BOB_NAME, CertificateType.SERVICE_IDENTITY) + val compositeKey = CompositeKey.Builder().addKey(notaryKey).build() + keyStore[DISTRIBUTED_NOTARY_COMPOSITE_KEY_ALIAS] = createNotaryCertificate(compositeKey, ALICE_NAME) + + assertThatThrownBy { + keyStoreHandler.init() + }.hasMessageContaining("The configured legalName").hasMessageContaining("doesn't match what's in the key store") + } +} \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/internal/NodeKeyStoreUtilitiesTest.kt b/node/src/test/kotlin/net/corda/node/internal/NodeKeyStoreUtilitiesTest.kt deleted file mode 100644 index d5c96cd080..0000000000 --- a/node/src/test/kotlin/net/corda/node/internal/NodeKeyStoreUtilitiesTest.kt +++ /dev/null @@ -1,157 +0,0 @@ -package net.corda.node.internal - -import com.nhaarman.mockito_kotlin.any -import com.nhaarman.mockito_kotlin.doAnswer -import com.nhaarman.mockito_kotlin.mock -import com.nhaarman.mockito_kotlin.verify -import com.nhaarman.mockito_kotlin.whenever -import net.corda.node.services.config.NodeConfiguration -import net.corda.nodeapi.internal.config.CertificateStore -import net.corda.nodeapi.internal.config.FileBasedCertificateStoreSupplier -import net.corda.nodeapi.internal.config.MutualSslConfiguration -import net.corda.nodeapi.internal.crypto.X509KeyStore -import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_CA -import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_TLS -import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA -import net.corda.nodeapi.internal.cryptoservice.CryptoService -import net.corda.nodeapi.internal.cryptoservice.bouncycastle.BCCryptoService -import net.corda.testing.core.ALICE_NAME -import org.assertj.core.api.Assertions.assertThat -import org.assertj.core.api.Assertions.assertThatThrownBy -import org.junit.Test -import java.io.IOException -import java.security.KeyStoreException -import java.security.cert.X509Certificate - -class NodeKeyStoreUtilitiesTest { - @Test(timeout = 300_000) - fun `initializing key store in non-dev mode with no key store`() { - whenever(signingSupplier.get()).doAnswer { throw IOException() } - - assertThatThrownBy { - config.initKeyStores(cryptoService) - }.hasMessageContaining("One or more keyStores (identity or TLS) or trustStore not found.") - } - - @Test(timeout = 300_000) - fun `initializing key store in non-dev mode with invalid password`() { - whenever(signingSupplier.get()).doAnswer { throw KeyStoreException() } - - assertThatThrownBy { - config.initKeyStores(cryptoService) - }.hasMessageContaining("At least one of the keystores or truststore passwords does not match configuration") - } - - @Test(timeout = 300_000) - fun `initializing key store in non-dev mode without trusted root`() { - whenever(trustStore.contains(CORDA_ROOT_CA)).thenReturn(false) - - assertThatThrownBy { - config.initKeyStores(cryptoService) - }.hasMessageContaining("Alias for trustRoot key not found. Please ensure you have an updated trustStore file") - } - - @Test(timeout = 300_000) - fun `initializing key store in non-dev mode without alias for TLS key`() { - whenever(keyStore.contains(CORDA_CLIENT_TLS)).thenReturn(false) - - assertThatThrownBy { - config.initKeyStores(cryptoService) - }.hasMessageContaining("Alias for TLS key not found. Please ensure you have an updated TLS keyStore file") - } - - @Test(timeout = 300_000) - fun `initializing key store in non-dev mode without alias for node CA key`() { - whenever(signingStore.contains(CORDA_CLIENT_CA)).thenReturn(false) - - assertThatThrownBy { - config.initKeyStores(cryptoService) - }.hasMessageContaining("Alias for Node CA key not found. Please ensure you have an updated identity keyStore file") - } - - @Test(timeout = 300_000) - fun `initializing key store should throw exception if cert path does not chain to the trust root`() { - val untrustedRoot = mock() - whenever(signingStore.query(any List>())).thenReturn(mutableListOf(untrustedRoot)) - - assertThatThrownBy { - config.initKeyStores(cryptoService) - }.hasMessageContaining("Client CA certificate must chain to the trusted root") - } - - @Test(timeout = 300_000) - fun `initializing key store should throw exception if TLS certificate does not chain to the trust root`() { - val untrustedRoot = mock() - whenever(keyStore.query(any List>())).thenReturn(mutableListOf(untrustedRoot)) - - assertThatThrownBy { - config.initKeyStores(cryptoService) - }.hasMessageContaining("TLS certificate must chain to the trusted root") - } - - @Test(timeout = 300_000) - fun `initializing key store should return valid certificate if certificate is valid`() { - val certificate = config.initKeyStores(cryptoService) - - assertThat(certificate).isEqualTo(trustRoot) - } - - @Test(timeout = 300_000) - fun `initializing key store in dev mode check te supplier`() { - whenever(config.devMode).thenReturn(true) - whenever(config.myLegalName).thenReturn(ALICE_NAME) - whenever(config.certificatesDirectory).thenReturn(mock()) - whenever(trustSupplier.getOptional()).thenReturn(mock()) - whenever(keySupplier.getOptional()).thenReturn(mock()) - whenever(signingSupplier.getOptional()).thenReturn(mock()) - - config.initKeyStores(cryptoService) - - verify(signingSupplier).getOptional() - } - - @Test(timeout = 300_000) - fun `initializing key store in dev mode with BCCryptoService call resyncKeystore`() { - val bCryptoService = mock() - whenever(config.devMode).thenReturn(true) - whenever(config.myLegalName).thenReturn(ALICE_NAME) - whenever(config.certificatesDirectory).thenReturn(mock()) - whenever(trustSupplier.getOptional()).thenReturn(mock()) - whenever(keySupplier.getOptional()).thenReturn(mock()) - whenever(signingSupplier.getOptional()).thenReturn(mock()) - - config.initKeyStores(bCryptoService) - - verify(bCryptoService).resyncKeystore() - } - - private val config = mock() - - private val trustStore = mock() - private val signingStore = mock() - private val keyStore = mock() - private val sslOptions = mock() - private val trustSupplier = mock() - private val signingSupplier = mock() - private val keySupplier = mock() - private val trustRoot = mock() - private val cryptoService = mock() - - init { - whenever(config.devMode).thenReturn(false) - - whenever(sslOptions.keyStore).thenReturn(keySupplier) - whenever(sslOptions.trustStore).thenReturn(trustSupplier) - whenever(config.signingCertificateStore).thenReturn(signingSupplier) - whenever(trustSupplier.get()).thenReturn(trustStore) - whenever(signingSupplier.get()).thenReturn(signingStore) - whenever(keySupplier.get()).thenReturn(keyStore) - whenever(trustStore.contains(CORDA_ROOT_CA)).thenReturn(true) - whenever(keyStore.contains(CORDA_CLIENT_TLS)).thenReturn(true) - whenever(signingStore.contains(CORDA_CLIENT_CA)).thenReturn(true) - whenever(config.p2pSslOptions).thenReturn(sslOptions) - whenever(trustStore[CORDA_ROOT_CA]).thenReturn(trustRoot) - whenever(signingStore.query(any List>())).thenReturn(mutableListOf(trustRoot)) - whenever(keyStore.query(any List>())).thenReturn(mutableListOf(trustRoot)) - } -} 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 333e356b1a..1cdd2f405f 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 @@ -65,7 +65,10 @@ class PersistentIdentityServiceTests { ) identityService.database = database identityService.ourNames = setOf(ALICE_NAME) - identityService.start(DEV_ROOT_CA.certificate, pkToIdCache = PublicKeyToOwningIdentityCacheImpl(database, cacheFactory)) + identityService.start(DEV_ROOT_CA.certificate, alice.identity, pkToIdCache = PublicKeyToOwningIdentityCacheImpl( + database, + cacheFactory + )) } @After @@ -230,7 +233,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, pkToIdCache = PublicKeyToOwningIdentityCacheImpl(database, cacheFactory)) + it.start(DEV_ROOT_CA.certificate, Companion.alice.identity, pkToIdCache = PublicKeyToOwningIdentityCacheImpl(database, cacheFactory)) } newPersistentIdentityService.assertOwnership(alice.party, anonymousAlice.party.anonymise()) 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 625d4f18be..1902001295 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,7 +6,6 @@ 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 @@ -188,7 +187,7 @@ open class MockServices private constructor( identityService.apply { ourNames = setOf(initialIdentity.name) database = persistence - start(DEV_ROOT_CA.certificate, pkToIdCache = pkToIdCache) + start(DEV_ROOT_CA.certificate, initialIdentity.identity, pkToIdCache = pkToIdCache) persistence.transaction { identityService.loadIdentities(moreIdentities + initialIdentity.identity) } } @@ -199,11 +198,11 @@ open class MockServices private constructor( val aliasedMoreKeys = moreKeys.mapIndexed { index, keyPair -> val alias = "Extra key $index" aliasKeyMap[alias] = keyPair - KeyPair(keyPair.public, AliasPrivateKey(alias)) - }.toSet() + keyPair.public to alias + } val identityAlias = "${initialIdentity.name} private key" aliasKeyMap[identityAlias] = initialIdentity.keyPair - val aliasedIdentityKey = KeyPair(initialIdentity.publicKey, AliasPrivateKey(identityAlias)) + val aliasedIdentityKey = initialIdentity.publicKey to identityAlias val keyManagementService = BasicHSMKeyManagementService( TestingNamedCacheFactory(), identityService, diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index f11a717370..c8f6515786 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -384,7 +384,10 @@ class DriverDSLImpl( NodeRegistrationConfiguration(config.corda), HTTPNetworkRegistrationService(networkServicesConfig, versionInfo), NodeRegistrationOption(rootTruststorePath, rootTruststorePassword) - ).generateKeysAndRegister() + ).apply { + generateKeysAndRegister() + generateNodeIdentity() + } config } } else { 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 4ba9b297ef..874dddf8e0 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 @@ -4,7 +4,6 @@ import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.whenever import net.corda.common.configuration.parsing.internal.ConfigurationWithOptions import net.corda.core.DoNotImplement -import net.corda.core.crypto.Crypto import net.corda.core.crypto.SecureHash import net.corda.core.crypto.random63BitValue import net.corda.core.flows.FlowLogic @@ -46,7 +45,6 @@ import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor import net.corda.node.utilities.DefaultNamedCacheFactory import net.corda.nodeapi.internal.DevIdentityGenerator import net.corda.nodeapi.internal.config.User -import net.corda.nodeapi.internal.cryptoservice.bouncycastle.BCCryptoService import net.corda.nodeapi.internal.network.NetworkParametersCopier import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig @@ -65,10 +63,10 @@ import java.math.BigInteger import java.net.URLClassLoader import java.nio.file.Path import java.nio.file.Paths -import java.security.PublicKey import java.time.Clock import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.atomic.AtomicReference val MOCK_VERSION_INFO = VersionInfo(PLATFORM_VERSION, "Mock release", "Mock revision", "Mock Vendor") @@ -334,8 +332,7 @@ open class InternalMockNetwork(cordappPackages: List = emptyList(), require(id >= 0) { "Node ID must be zero or positive, was passed: $id" } } - private val entropyRoot = args.entropyRoot - var counter = entropyRoot + private val entropyCounter = AtomicReference(args.entropyRoot) override val log get() = staticLog override val transactionVerifierWorkerCount: Int get() = 1 @@ -403,15 +400,7 @@ open class InternalMockNetwork(cordappPackages: List = emptyList(), //No mock shell } - // This is not thread safe, but node construction is done on a single thread, so that should always be fine - override fun generateKeyPair(alias: String): PublicKey { - require(cryptoService is BCCryptoService) { "MockNode supports BCCryptoService only, but it is ${cryptoService.javaClass.name}" } - counter = counter.add(BigInteger.ONE) - // The StartedMockNode specifically uses EdDSA keys as they are fixed and stored in json files for some tests (e.g IRSSimulation). - val keyPair = Crypto.deriveKeyPairFromEntropy(Crypto.EDDSA_ED25519_SHA512, counter) - (cryptoService as BCCryptoService).importKey(alias, keyPair) - return keyPair.public - } + override fun initKeyStores() = keyStoreHandler.init(entropyCounter.updateAndGet { it.add(BigInteger.ONE) }) // NodeInfo requires a non-empty addresses list and so we give it a dummy value for mock nodes. // The non-empty addresses check is important to have and so we tolerate the ugliness here. 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 0f8c3c505f..4db4ad09c1 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 @@ -41,8 +41,8 @@ class MockKeyManagementService( override fun getSigner(publicKey: PublicKey): ContentSigner = net.corda.node.services.keys.getSigner(getSigningKeyPair(publicKey)) - override fun start(initialKeyPairs: Set) { - initialKeyPairs.forEach { keyStore[it.public] = it.private } + override fun start(initialKeysAndAliases: Iterable>) { + throw NotImplementedError() } private fun getSigningKeyPair(publicKey: PublicKey): KeyPair {