From c533792f3f7015d07d792a849f4f610f3e23cced Mon Sep 17 00:00:00 2001 From: JamesHR3 <45565019+JamesHR3@users.noreply.github.com> Date: Tue, 7 May 2019 11:49:29 +0100 Subject: [PATCH] [CORDA-2866] Prevent node startup if legal identity key is lost but node key isn't (#5090) --- .../nodeapi/internal/DevIdentityGenerator.kt | 2 +- .../nodeapi/internal/KeyStoreConfigHelpers.kt | 10 ++--- .../internal/crypto/X509UtilitiesTest.kt | 10 +++-- .../kotlin/net/corda/node/BootTests.kt | 37 ++++++++++++++++--- .../net/corda/node/amqp/ProtonWrapperTests.kt | 4 +- .../services/statemachine/HardRestartTest.kt | 8 ++-- .../net/corda/node/internal/AbstractNode.kt | 14 ++++++- .../node/services/config/ConfigUtilities.kt | 9 +++-- 8 files changed, 68 insertions(+), 26 deletions(-) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/DevIdentityGenerator.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/DevIdentityGenerator.kt index a4c352fcca..a4aa9175a4 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/DevIdentityGenerator.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/DevIdentityGenerator.kt @@ -38,7 +38,7 @@ object DevIdentityGenerator { val p2pSslConfig = SslConfiguration.mutual(p2pKeyStore, p2pTrustStore) certificatesDirectory.createDirectories() - val nodeKeyStore = signingCertStore.get(true).also { it.registerDevSigningCertificates(legalName) } + val nodeKeyStore = signingCertStore.get(true).also { it.installDevNodeCaCertPath(legalName) } p2pSslConfig.keyStore.get(true).also { it.registerDevP2pCertificates(legalName) } val identity = nodeKeyStore.storeLegalIdentity("$NODE_IDENTITY_ALIAS_PREFIX-private-key") diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/KeyStoreConfigHelpers.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/KeyStoreConfigHelpers.kt index 18432d2a8b..76bf478b31 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/KeyStoreConfigHelpers.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/KeyStoreConfigHelpers.kt @@ -26,14 +26,14 @@ import javax.security.auth.x500.X500Principal * the given legal name), and the SSL key store will store the TLS cert which is a sub-cert of the node CA. */ -fun CertificateStore.registerDevSigningCertificates(legalName: CordaX500Name, - rootCert: X509Certificate = DEV_ROOT_CA.certificate, - intermediateCa: CertificateAndKeyPair = DEV_INTERMEDIATE_CA, - devNodeCa: CertificateAndKeyPair = createDevNodeCa(intermediateCa, legalName)) { +fun CertificateStore.installDevNodeCaCertPath(legalName: CordaX500Name, + rootCert: X509Certificate = DEV_ROOT_CA.certificate, + intermediateCa: CertificateAndKeyPair = DEV_INTERMEDIATE_CA, + devNodeCa: CertificateAndKeyPair = createDevNodeCa(intermediateCa, legalName)) { update { setPrivateKey(X509Utilities.CORDA_CLIENT_CA, devNodeCa.keyPair.private, listOf(devNodeCa.certificate, intermediateCa.certificate, rootCert), - this@registerDevSigningCertificates.entryPassword) + this@installDevNodeCaCertPath.entryPassword) } } diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt index ee0676639a..2e59d40af1 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt @@ -1,6 +1,6 @@ package net.corda.nodeapi.internal.crypto -import net.corda.core.crypto.* +import net.corda.core.crypto.Crypto import net.corda.core.crypto.Crypto.COMPOSITE_KEY import net.corda.core.crypto.Crypto.ECDSA_SECP256K1_SHA256 import net.corda.core.crypto.Crypto.ECDSA_SECP256R1_SHA256 @@ -8,6 +8,8 @@ import net.corda.core.crypto.Crypto.EDDSA_ED25519_SHA512 import net.corda.core.crypto.Crypto.RSA_SHA256 import net.corda.core.crypto.Crypto.SPHINCS256_SHA256 import net.corda.core.crypto.Crypto.generateKeyPair +import net.corda.core.crypto.SignatureScheme +import net.corda.core.crypto.newSecureRandom import net.corda.core.identity.CordaX500Name import net.corda.core.internal.div import net.corda.core.serialization.SerializationContext @@ -18,9 +20,9 @@ import net.corda.nodeapi.internal.config.MutualSslConfiguration import net.corda.nodeapi.internal.createDevNodeCa import net.corda.nodeapi.internal.crypto.X509Utilities.DEFAULT_IDENTITY_SIGNATURE_SCHEME import net.corda.nodeapi.internal.crypto.X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME +import net.corda.nodeapi.internal.installDevNodeCaCertPath import net.corda.nodeapi.internal.protonwrapper.netty.init import net.corda.nodeapi.internal.registerDevP2pCertificates -import net.corda.nodeapi.internal.registerDevSigningCertificates import net.corda.serialization.internal.AllWhitelist import net.corda.serialization.internal.SerializationContextImpl import net.corda.serialization.internal.SerializationFactoryImpl @@ -28,8 +30,8 @@ import net.corda.serialization.internal.amqp.amqpMagic import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import net.corda.testing.core.TestIdentity -import net.corda.testing.internal.stubs.CertificateStoreStubs import net.corda.testing.internal.createDevIntermediateCaCertPath +import net.corda.testing.internal.stubs.CertificateStoreStubs import net.i2p.crypto.eddsa.EdDSAPrivateKey import org.assertj.core.api.Assertions.assertThat import org.bouncycastle.asn1.x509.* @@ -232,7 +234,7 @@ class X509UtilitiesTest { // Generate server cert and private key and populate another keystore suitable for SSL val nodeCa = createDevNodeCa(intermediateCa, MEGA_CORP.name) - signingCertStore.get(createNew = true).also { it.registerDevSigningCertificates(MEGA_CORP.name, rootCa.certificate, intermediateCa, nodeCa) } + signingCertStore.get(createNew = true).also { it.installDevNodeCaCertPath(MEGA_CORP.name, rootCa.certificate, intermediateCa, nodeCa) } p2pSslConfig.keyStore.get(createNew = true).also { it.registerDevP2pCertificates(MEGA_CORP.name, rootCa.certificate, intermediateCa, nodeCa) } // Load back server certificate val certStore = signingCertStore.get() diff --git a/node/src/integration-test/kotlin/net/corda/node/BootTests.kt b/node/src/integration-test/kotlin/net/corda/node/BootTests.kt index 25101f6ad4..dd1d5b7965 100644 --- a/node/src/integration-test/kotlin/net/corda/node/BootTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/BootTests.kt @@ -5,27 +5,31 @@ import net.corda.client.rpc.CordaRPCClient import net.corda.core.CordaRuntimeException import net.corda.core.flows.FlowLogic import net.corda.core.flows.StartableByRPC -import net.corda.core.internal.div -import net.corda.core.internal.isRegularFile -import net.corda.core.internal.list -import net.corda.core.internal.readLines +import net.corda.core.internal.* import net.corda.core.messaging.startFlow import net.corda.core.utilities.getOrThrow import net.corda.node.internal.NodeStartup import net.corda.node.services.Permissions.Companion.startFlow import net.corda.nodeapi.exceptions.InternalNodeException +import net.corda.nodeapi.internal.crypto.X509Utilities.NODE_IDENTITY_ALIAS_PREFIX +import net.corda.nodeapi.internal.installDevNodeCaCertPath import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.NodeParameters import net.corda.testing.driver.driver +import net.corda.testing.internal.stubs.CertificateStoreStubs import net.corda.testing.node.User import net.corda.testing.node.internal.startNode import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.Test -import java.io.* +import java.io.ByteArrayOutputStream +import java.io.ObjectInputStream +import java.io.ObjectOutputStream +import java.io.Serializable import kotlin.test.assertEquals +import kotlin.test.assertTrue class BootTests { @Test @@ -64,6 +68,29 @@ class BootTests { assertEquals(1, numberOfNodesThatLogged) } } + + @Test + fun `node fails to start if legal identity is lost`() { + driver(DriverParameters(notarySpecs = emptyList(), inMemoryDB = false, startNodesInProcess = false)) { + val alice = startNode(providedName = ALICE_NAME).getOrThrow() + val aliceCertDir = alice.baseDirectory / "certificates" + (aliceCertDir / "nodekeystore.jks").delete() + val cert = CertificateStoreStubs.Signing.withCertificatesDirectory(aliceCertDir).get(true) + // Creating a new certificate store does not populate that store with the node certificate path. If the node certificate path is + // missing, the node will fail to start but not because the legal identity is missing. To test that a missing legal identity + // prevents the node from starting, the node certificate path must be installed. + cert.installDevNodeCaCertPath(ALICE_NAME) + alice.stop() + // The node shouldn't start, and the logs should indicate that the failure is due to a missing identity key + assertThatThrownBy { + startNode(providedName = ALICE_NAME).getOrThrow() + } + val logFolder = alice.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME + val logFile = logFolder.list { it.filter { a -> a.isRegularFile() && a.fileName.toString().startsWith("node") }.findFirst().get() } + val lines = logFile.readLines { lines -> lines.filter { "$NODE_IDENTITY_ALIAS_PREFIX-private-key" in it }.toArray() } + assertTrue(lines.count() > 0) + } + } } @StartableByRPC diff --git a/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt b/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt index f9336d6b19..22068615e5 100644 --- a/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt @@ -17,13 +17,13 @@ import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX import net.corda.nodeapi.internal.ArtemisTcpTransport import net.corda.nodeapi.internal.config.MutualSslConfiguration import net.corda.nodeapi.internal.crypto.X509Utilities +import net.corda.nodeapi.internal.installDevNodeCaCertPath import net.corda.nodeapi.internal.protonwrapper.messages.MessageStatus import net.corda.nodeapi.internal.protonwrapper.netty.AMQPClient import net.corda.nodeapi.internal.protonwrapper.netty.AMQPConfiguration import net.corda.nodeapi.internal.protonwrapper.netty.AMQPServer import net.corda.nodeapi.internal.protonwrapper.netty.init import net.corda.nodeapi.internal.registerDevP2pCertificates -import net.corda.nodeapi.internal.registerDevSigningCertificates import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import net.corda.testing.core.CHARLIE_NAME @@ -133,7 +133,7 @@ class ProtonWrapperTests { val (rootCa, intermediateCa) = createDevIntermediateCaCertPath() // Generate server cert and private key and populate another keystore suitable for SSL - signingCertificateStore.get(true).also { it.registerDevSigningCertificates(ALICE_NAME, rootCa.certificate, intermediateCa) } + signingCertificateStore.get(true).also { it.installDevNodeCaCertPath(ALICE_NAME, rootCa.certificate, intermediateCa) } sslConfig.keyStore.get(true).also { it.registerDevP2pCertificates(ALICE_NAME, rootCa.certificate, intermediateCa) } sslConfig.createTrustStore(rootCa.certificate) diff --git a/node/src/integration-test/kotlin/net/corda/node/services/statemachine/HardRestartTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/statemachine/HardRestartTest.kt index 10e5f4ba2c..b8876c70ce 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/statemachine/HardRestartTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/statemachine/HardRestartTest.kt @@ -84,7 +84,7 @@ class HardRestartTest { Thread.sleep(ms.toLong()) (b as OutOfProcess).process.destroyForcibly() b.stop() - startNode(providedName = DUMMY_BANK_B_NAME, rpcUsers = listOf(demoUser), customOverrides = mapOf("p2pAddress" to "localhost:${b.rpcAddress.port}")) + startNode(providedName = DUMMY_BANK_B_NAME, rpcUsers = listOf(demoUser)) } CordaRPCClient(a.rpcAddress).use(demoUser.username, demoUser.password) { val returnValue = it.proxy.startFlow(::Ping, b.nodeInfo.singleIdentity(), 1).returnValue @@ -122,7 +122,7 @@ class HardRestartTest { Thread.sleep(ms.toLong()) (b as OutOfProcess).process.destroyForcibly() b.stop() - startNode(providedName = DUMMY_BANK_B_NAME, rpcUsers = listOf(demoUser), customOverrides = mapOf("p2pAddress" to "localhost:${b.rpcAddress.port}")) + startNode(providedName = DUMMY_BANK_B_NAME, rpcUsers = listOf(demoUser)) } CordaRPCClient(a.rpcAddress).use(demoUser.username, demoUser.password) { val returnValue = it.proxy.startFlow(::Ping, b.nodeInfo.singleIdentity(), 100).returnValue @@ -159,7 +159,7 @@ class HardRestartTest { println("Sleeping $ms ms before kill") Thread.sleep(ms.toLong()) b.stop() - startNode(providedName = DUMMY_BANK_B_NAME, rpcUsers = listOf(demoUser), customOverrides = mapOf("p2pAddress" to "localhost:${b.rpcAddress.port}")) + startNode(providedName = DUMMY_BANK_B_NAME, rpcUsers = listOf(demoUser)) } CordaRPCClient(a.rpcAddress).use(demoUser.username, demoUser.password) { val returnValue = it.proxy.startFlow(::Ping, b.nodeInfo.singleIdentity(), 100).returnValue @@ -242,7 +242,7 @@ class HardRestartTest { Thread.sleep(ms.toLong()) (b as OutOfProcess).process.destroyForcibly() b.stop() - startNode(providedName = DUMMY_BANK_B_NAME, rpcUsers = listOf(demoUser), customOverrides = mapOf("p2pAddress" to "localhost:${b.rpcAddress.port}")) + startNode(providedName = DUMMY_BANK_B_NAME, rpcUsers = listOf(demoUser)) } val executor = Executors.newFixedThreadPool(8) try { 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 df1b8a7d31..cd74679896 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -55,7 +55,6 @@ import net.corda.node.services.events.ScheduledActivityObserver import net.corda.node.services.identity.PersistentIdentityService import net.corda.node.services.keys.BasicHSMKeyManagementService import net.corda.node.services.keys.KeyManagementServiceInternal -import net.corda.nodeapi.internal.cryptoservice.bouncycastle.BCCryptoService import net.corda.node.services.messaging.DeduplicationHandler import net.corda.node.services.messaging.MessagingService import net.corda.node.services.network.NetworkMapClient @@ -82,6 +81,7 @@ 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.X509Utilities.DISTRIBUTED_NOTARY_ALIAS_PREFIX import net.corda.nodeapi.internal.crypto.X509Utilities.NODE_IDENTITY_ALIAS_PREFIX +import net.corda.nodeapi.internal.cryptoservice.bouncycastle.BCCryptoService import net.corda.nodeapi.internal.persistence.* import net.corda.tools.shell.InteractiveShell import org.apache.activemq.artemis.utils.ReusableLatch @@ -286,8 +286,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration, check(started == null) { "Node has already been started" } log.info("Generating nodeInfo ...") val trustRoot = initKeyStores() - val (identity, identityKeyPair) = obtainIdentity() startDatabase() + val (identity, identityKeyPair) = obtainIdentity() val nodeCa = configuration.signingCertificateStore.get()[CORDA_CLIENT_CA] identityService.start(trustRoot, listOf(identity.certificate, nodeCa)) return database.use { @@ -838,6 +838,16 @@ abstract class AbstractNode(val configuration: NodeConfiguration, 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.createX500Map(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]. 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 15fbc4cf95..8815171fae 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 @@ -8,13 +8,16 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.internal.createDirectories import net.corda.core.internal.div import net.corda.core.internal.exists -import net.corda.nodeapi.internal.cryptoservice.bouncycastle.BCCryptoService -import net.corda.nodeapi.internal.* +import net.corda.nodeapi.internal.DEV_CA_KEY_STORE_PASS 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.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 org.slf4j.LoggerFactory import java.nio.file.Path @@ -92,7 +95,7 @@ fun MutualSslConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500N when (cryptoService) { is BCCryptoService, null -> { val signingKeyStore = FileBasedCertificateStoreSupplier(signingCertificateStore.path, signingCertificateStore.storePassword, signingCertificateStore.entryPassword).get(true) - .also { it.registerDevSigningCertificates(myLegalName) } + .also { it.installDevNodeCaCertPath(myLegalName) } // Move distributed service composite key (generated by IdentityGenerator.generateToDisk) to keystore if exists. val distributedServiceKeystore = certificatesDirectory / "distributedService.jks"