diff --git a/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt b/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt index ea71007414..560987cdf7 100644 --- a/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt @@ -1,6 +1,5 @@ package net.corda.node.utilities.registration -import net.corda.core.crypto.Crypto import net.corda.core.identity.CordaX500Name import net.corda.core.internal.concurrent.transpose import net.corda.core.messaging.startFlow @@ -26,7 +25,6 @@ import net.corda.testing.node.internal.CompatibilityZoneParams import net.corda.testing.node.internal.internalDriver import net.corda.testing.node.internal.network.NetworkMapServer import org.assertj.core.api.Assertions.assertThat -import org.assertj.core.api.Assertions.assertThatThrownBy import org.bouncycastle.pkcs.PKCS10CertificationRequest import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest import org.junit.After @@ -38,12 +36,10 @@ import java.io.InputStream import java.net.URL import java.security.KeyPair import java.security.cert.CertPath -import java.security.cert.CertPathValidatorException import java.security.cert.Certificate import java.security.cert.X509Certificate import java.util.zip.ZipEntry import java.util.zip.ZipOutputStream -import javax.security.auth.x500.X500Principal import javax.ws.rs.* import javax.ws.rs.core.MediaType import javax.ws.rs.core.Response @@ -116,28 +112,6 @@ class NodeRegistrationTest { ).returnValue.getOrThrow() } } - - @Test - fun `node registration wrong root cert`() { - val someRootCert = X509Utilities.createSelfSignedCACertificate( - X500Principal("CN=Integration Test Corda Node Root CA,O=R3 Ltd,L=London,C=GB"), - Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)) - val compatibilityZone = CompatibilityZoneParams( - URL("http://$serverHostAndPort"), - publishNotaries = { server.networkParameters = testNetworkParameters(it) }, - rootCert = someRootCert) - internalDriver( - portAllocation = portAllocation, - compatibilityZone = compatibilityZone, - initialiseSerialization = false, - notarySpecs = listOf(NotarySpec(notaryName)), - startNodesInProcess = true // We need to run the nodes in the same process so that we can capture the correct exception - ) { - assertThatThrownBy { - defaultNotaryNode.getOrThrow() - }.isInstanceOf(CertPathValidatorException::class.java) - } - } } @Path("certificate") diff --git a/node/src/main/kotlin/net/corda/node/ArgsParser.kt b/node/src/main/kotlin/net/corda/node/ArgsParser.kt index c6667d6a11..8353dd7f1a 100644 --- a/node/src/main/kotlin/net/corda/node/ArgsParser.kt +++ b/node/src/main/kotlin/net/corda/node/ArgsParser.kt @@ -1,6 +1,5 @@ package net.corda.node -import com.typesafe.config.ConfigException import joptsimple.OptionParser import joptsimple.util.EnumConverter import net.corda.core.internal.div @@ -34,6 +33,10 @@ class ArgsParser { private val sshdServerArg = optionParser.accepts("sshd", "Enables SSHD server for node administration.") private val noLocalShellArg = optionParser.accepts("no-local-shell", "Do not start the embedded shell locally.") private val isRegistrationArg = optionParser.accepts("initial-registration", "Start initial node registration with Corda network to obtain certificate from the permissioning server.") + private val networkRootTruststorePathArg = optionParser.accepts("network-root-truststore", "Network root trust store obtained from network operator.") + .withRequiredArg() + private val networkRootTruststorePasswordArg = optionParser.accepts("network-root-truststore-password", "Network root trust store password obtained from network operator.") + .withRequiredArg() private val isVersionArg = optionParser.accepts("version", "Print the version and exit") private val justGenerateNodeInfoArg = optionParser.accepts("just-generate-node-info", "Perform the node start-up task necessary to generate its nodeInfo, save it to disk, then quit") @@ -56,8 +59,21 @@ class ArgsParser { val sshdServer = optionSet.has(sshdServerArg) val justGenerateNodeInfo = optionSet.has(justGenerateNodeInfoArg) val bootstrapRaftCluster = optionSet.has(bootstrapRaftClusterArg) - return CmdLineOptions(baseDirectory, configFile, help, loggingLevel, logToConsole, isRegistration, isVersion, - noLocalShell, sshdServer, justGenerateNodeInfo, bootstrapRaftCluster) + val networkRootTruststorePath = optionSet.valueOf(networkRootTruststorePathArg)?.let { Paths.get(it).normalize().toAbsolutePath() } + val networkRootTruststorePassword = optionSet.valueOf(networkRootTruststorePasswordArg) + return CmdLineOptions(baseDirectory, + configFile, + help, + loggingLevel, + logToConsole, + isRegistration, + networkRootTruststorePath, + networkRootTruststorePassword, + isVersion, + noLocalShell, + sshdServer, + justGenerateNodeInfo, + bootstrapRaftCluster) } fun printHelp(sink: PrintStream) = optionParser.printHelpOn(sink) @@ -69,6 +85,8 @@ data class CmdLineOptions(val baseDirectory: Path, val loggingLevel: Level, val logToConsole: Boolean, val isRegistration: Boolean, + val networkRootTruststorePath: Path?, + val networkRootTruststorePassword: String?, val isVersion: Boolean, val noLocalShell: Boolean, val sshdServer: Boolean, @@ -78,6 +96,8 @@ data class CmdLineOptions(val baseDirectory: Path, val config = ConfigHelper.loadConfig(baseDirectory, configFile).parseAsNodeConfiguration() if (isRegistration) { requireNotNull(config.compatibilityZoneURL) { "Compatibility Zone Url must be provided in registration mode." } + requireNotNull(networkRootTruststorePath) { "Network root trust store path must be provided in registration mode." } + requireNotNull(networkRootTruststorePassword) { "Network root trust store password must be provided in registration mode." } } return config } diff --git a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt index 7f3d45ffea..fa3ea52d3a 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -2,8 +2,11 @@ package net.corda.node.internal import com.jcabi.manifests.Manifests import joptsimple.OptionException -import net.corda.core.internal.* +import net.corda.core.internal.Emoji import net.corda.core.internal.concurrent.thenMatch +import net.corda.core.internal.createDirectories +import net.corda.core.internal.div +import net.corda.core.internal.randomOrNull import net.corda.core.utilities.loggerFor import net.corda.node.* import net.corda.node.services.config.NodeConfiguration @@ -91,7 +94,8 @@ open class NodeStartup(val args: Array) { banJavaSerialisation(conf) preNetworkRegistration(conf) if (shouldRegisterWithNetwork(cmdlineOptions, conf)) { - registerWithNetwork(cmdlineOptions, conf) + // Null checks for [compatibilityZoneURL], [rootTruststorePath] and [rootTruststorePassword] has been done in [CmdLineOptions.loadConfig] + registerWithNetwork(conf, cmdlineOptions.networkRootTruststorePath!!, cmdlineOptions.networkRootTruststorePassword!!) return true } logStartupInfo(versionInfo, cmdlineOptions, conf) @@ -179,7 +183,7 @@ open class NodeStartup(val args: Array) { return !(!cmdlineOptions.isRegistration || compatibilityZoneURL == null) } - open protected fun registerWithNetwork(cmdlineOptions: CmdLineOptions, conf: NodeConfiguration) { + open protected fun registerWithNetwork(conf: NodeConfiguration, networkRootTruststorePath: Path, networkRootTruststorePassword: String) { val compatibilityZoneURL = conf.compatibilityZoneURL!! println() println("******************************************************************") @@ -187,7 +191,7 @@ open class NodeStartup(val args: Array) { println("* Registering as a new participant with Corda network *") println("* *") println("******************************************************************") - NetworkRegistrationHelper(conf, HTTPNetworkRegistrationService(compatibilityZoneURL)).buildKeystore() + NetworkRegistrationHelper(conf, HTTPNetworkRegistrationService(compatibilityZoneURL), networkRootTruststorePath, networkRootTruststorePassword).buildKeystore() } open protected fun loadConfigFile(cmdlineOptions: CmdLineOptions): NodeConfiguration = cmdlineOptions.loadConfig() 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 509ade8a32..a940a28a05 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 @@ -6,14 +6,15 @@ import net.corda.core.internal.* import net.corda.core.utilities.seconds import net.corda.node.services.config.NodeConfiguration import net.corda.nodeapi.internal.crypto.CertificateType +import net.corda.nodeapi.internal.crypto.X509KeyStore 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.x509 import org.bouncycastle.openssl.jcajce.JcaPEMWriter import org.bouncycastle.util.io.pem.PemObject import java.io.StringWriter +import java.nio.file.Path import java.security.KeyPair import java.security.KeyStore import java.security.cert.X509Certificate @@ -22,7 +23,10 @@ import java.security.cert.X509Certificate * Helper for managing the node registration process, which checks for any existing certificates and requests them if * needed. */ -class NetworkRegistrationHelper(private val config: NodeConfiguration, private val certService: NetworkRegistrationService) { +class NetworkRegistrationHelper(private val config: NodeConfiguration, + private val certService: NetworkRegistrationService, + networkRootTrustStorePath: Path, + networkRootTruststorePassword: String) { private companion object { val pollInterval = 10.seconds const val SELF_SIGNED_PRIVATE_KEY = "Self Signed Private Key" @@ -31,20 +35,16 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration, private v private val requestIdStore = config.certificatesDirectory / "certificate-request-id.txt" // TODO: Use different password for private key. private val privateKeyPassword = config.keyStorePassword + private val rootTrustStore: X509KeyStore private val rootCert: X509Certificate init { - require(config.trustStoreFile.exists()) { - "${config.trustStoreFile} does not exist. This file must contain the root CA cert of your compatibility zone. " + + require(networkRootTrustStorePath.exists()) { + "$networkRootTrustStorePath does not exist. This file must contain the root CA cert of your compatibility zone. " + "Please contact your CZ operator." } - val rootCert = config.loadTrustStore().internal.getCertificate(CORDA_ROOT_CA) - require(rootCert != null) { - "${config.trustStoreFile} does not contain a certificate with the key $CORDA_ROOT_CA." + - "This file must contain the root CA cert of your compatibility zone. " + - "Please contact your CZ operator." - } - this.rootCert = rootCert.x509 + rootTrustStore = X509KeyStore.fromFile(networkRootTrustStorePath, networkRootTruststorePassword) + rootCert = rootTrustStore.getCertificate(CORDA_ROOT_CA) } /** @@ -109,7 +109,7 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration, private v throw CertificateRequestException("Received node CA cert has invalid role: $nodeCaCertRole") } - println("Checking root of the certificate path is what we expect.") + // Validate certificate chain returned from the doorman with the root cert obtained via out-of-band process, to prevent MITM attack on doorman server. X509Utilities.validateCertificateChain(rootCert, certificates) println("Certificate signing request approved, storing private key with the certificate chain.") @@ -119,6 +119,14 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration, private v nodeKeyStore.save() println("Node private key and certificate stored in ${config.nodeKeystore}.") + // Save root certificates to trust store. + config.loadTrustStore(createNew = true).update { + println("Generating trust store for corda node.") + // Assumes certificate chain always starts with client certificate and end with root certificate. + setCertificate(CORDA_ROOT_CA, certificates.last()) + } + println("Node trust store stored in ${config.trustStoreFile}.") + config.loadSslKeyStore(createNew = true).update { println("Generating SSL certificate for node messaging service.") val sslKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) diff --git a/node/src/test/kotlin/net/corda/node/ArgsParserTest.kt b/node/src/test/kotlin/net/corda/node/ArgsParserTest.kt index 8ba6f00b27..b5f56c4454 100644 --- a/node/src/test/kotlin/net/corda/node/ArgsParserTest.kt +++ b/node/src/test/kotlin/net/corda/node/ArgsParserTest.kt @@ -7,6 +7,7 @@ import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.junit.Test import org.slf4j.event.Level import java.nio.file.Paths +import kotlin.test.assertEquals class ArgsParserTest { private val parser = ArgsParser() @@ -25,7 +26,9 @@ class ArgsParserTest { noLocalShell = false, sshdServer = false, justGenerateNodeInfo = false, - bootstrapRaftCluster = false)) + bootstrapRaftCluster = false, + networkRootTruststorePassword = null, + networkRootTruststorePath = null)) } @Test @@ -110,8 +113,11 @@ class ArgsParserTest { @Test fun `initial-registration`() { - val cmdLineOptions = parser.parse("--initial-registration") + val cmdLineOptions = parser.parse("--initial-registration", "--network-root-truststore", "/truststore/file.jks", "--network-root-truststore-password", "password-test") assertThat(cmdLineOptions.isRegistration).isTrue() + assertEquals(Paths.get("/truststore/file.jks"), cmdLineOptions.networkRootTruststorePath) + assertEquals("password-test", cmdLineOptions.networkRootTruststorePassword) + } @Test diff --git a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt index b2788fcd25..e7e6775a5e 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt @@ -10,9 +10,11 @@ import net.corda.core.crypto.Crypto import net.corda.core.crypto.SecureHash import net.corda.core.identity.CordaX500Name import net.corda.core.internal.createDirectories +import net.corda.core.internal.div import net.corda.core.internal.x500Name import net.corda.node.services.config.NodeConfiguration import net.corda.nodeapi.internal.crypto.CertificateType +import net.corda.nodeapi.internal.crypto.X509KeyStore import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.testing.core.ALICE_NAME import net.corda.testing.internal.createDevIntermediateCaCertPath @@ -35,10 +37,13 @@ class NetworkRegistrationHelperTest { private val nodeLegalName = ALICE_NAME private lateinit var config: NodeConfiguration + private val networkRootTrustStoreFileName = "network-root-truststore.jks" + private val networkRootTrustStorePassword = "network-root-truststore-password" @Before fun init() { val baseDirectory = fs.getPath("/baseDir").createDirectories() + abstract class AbstractNodeConfiguration : NodeConfiguration config = rigorousMock().also { doReturn(baseDirectory).whenever(it).baseDirectory @@ -62,7 +67,7 @@ class NetworkRegistrationHelperTest { val nodeCaCertPath = createNodeCaCertPath() - saveTrustStoreWithRootCa(nodeCaCertPath.last()) + saveNetworkTrustStore(nodeCaCertPath.last()) createRegistrationHelper(nodeCaCertPath).buildKeystore() val nodeKeystore = config.loadNodeKeyStore() @@ -105,7 +110,7 @@ class NetworkRegistrationHelperTest { @Test fun `node CA with incorrect cert role`() { val nodeCaCertPath = createNodeCaCertPath(type = CertificateType.TLS) - saveTrustStoreWithRootCa(nodeCaCertPath.last()) + saveNetworkTrustStore(nodeCaCertPath.last()) val registrationHelper = createRegistrationHelper(nodeCaCertPath) assertThatExceptionOfType(CertificateRequestException::class.java) .isThrownBy { registrationHelper.buildKeystore() } @@ -116,7 +121,7 @@ class NetworkRegistrationHelperTest { fun `node CA with incorrect subject`() { val invalidName = CordaX500Name("Foo", "MU", "GB") val nodeCaCertPath = createNodeCaCertPath(legalName = invalidName) - saveTrustStoreWithRootCa(nodeCaCertPath.last()) + saveNetworkTrustStore(nodeCaCertPath.last()) val registrationHelper = createRegistrationHelper(nodeCaCertPath) assertThatExceptionOfType(CertificateRequestException::class.java) .isThrownBy { registrationHelper.buildKeystore() } @@ -128,7 +133,7 @@ class NetworkRegistrationHelperTest { val wrongRootCert = X509Utilities.createSelfSignedCACertificate( X500Principal("O=Foo,L=MU,C=GB"), Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)) - saveTrustStoreWithRootCa(wrongRootCert) + saveNetworkTrustStore(wrongRootCert) val registrationHelper = createRegistrationHelper(createNodeCaCertPath()) assertThatThrownBy { registrationHelper.buildKeystore() @@ -155,12 +160,13 @@ class NetworkRegistrationHelperTest { doReturn(requestId).whenever(it).submitRequest(any()) doReturn(response).whenever(it).retrieveCertificates(eq(requestId)) } - return NetworkRegistrationHelper(config, certService) + return NetworkRegistrationHelper(config, certService, config.certificatesDirectory / networkRootTrustStoreFileName, networkRootTrustStorePassword) } - private fun saveTrustStoreWithRootCa(rootCert: X509Certificate) { + private fun saveNetworkTrustStore(rootCert: X509Certificate) { config.certificatesDirectory.createDirectories() - config.loadTrustStore(createNew = true).update { + val rootTruststorePath = config.certificatesDirectory / networkRootTrustStoreFileName + X509KeyStore.fromFile(rootTruststorePath, networkRootTrustStorePassword, createNew = true).update { setCertificate(X509Utilities.CORDA_ROOT_CA, rootCert) } } 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 dab5cd7a68..3a41a4627a 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 @@ -33,6 +33,7 @@ import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.addShutdownHook import net.corda.nodeapi.internal.config.parseAs import net.corda.nodeapi.internal.config.toConfig +import net.corda.nodeapi.internal.crypto.X509KeyStore import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.network.NetworkParametersCopier import net.corda.nodeapi.internal.network.NodeInfoFilesCopier @@ -237,17 +238,24 @@ class DriverDSLImpl( )) config.corda.certificatesDirectory.createDirectories() - config.corda.loadTrustStore(createNew = true).update { + // Create network root truststore. + val rootTruststorePath = config.corda.certificatesDirectory / "network-root-truststore.jks" + // The network truststore will be provided by the network operator via out-of-band communication. + val rootTruststorePassword = "corda-root-password" + X509KeyStore.fromFile(rootTruststorePath, rootTruststorePassword, createNew = true).update { setCertificate(X509Utilities.CORDA_ROOT_CA, rootCert) } return if (startNodesInProcess) { executorService.fork { - NetworkRegistrationHelper(config.corda, HTTPNetworkRegistrationService(compatibilityZoneURL)).buildKeystore() + NetworkRegistrationHelper(config.corda, HTTPNetworkRegistrationService(compatibilityZoneURL), rootTruststorePath, rootTruststorePassword).buildKeystore() config } } else { - startOutOfProcessMiniNode(config, "--initial-registration").map { config } + startOutOfProcessMiniNode(config, + "--initial-registration", + "--network-root-truststore=${rootTruststorePath.toAbsolutePath()}", + "--network-root-truststore-password=$rootTruststorePassword").map { config } } } @@ -479,8 +487,8 @@ class DriverDSLImpl( when (it.cluster) { null -> startSingleNotary(it, localNetworkMap) is ClusterSpec.Raft, - // DummyCluster is used for testing the notary communication path, and it does not matter - // which underlying consensus algorithm is used, so we just stick to Raft + // DummyCluster is used for testing the notary communication path, and it does not matter + // which underlying consensus algorithm is used, so we just stick to Raft is DummyClusterSpec -> startRaftNotaryCluster(it, localNetworkMap) else -> throw IllegalArgumentException("BFT-SMaRt not supported") } @@ -596,7 +604,7 @@ class DriverDSLImpl( * Start the node with the given flag which is expected to start the node for some function, which once complete will * terminate the node. */ - private fun startOutOfProcessMiniNode(config: NodeConfig, extraCmdLineFlag: String): CordaFuture { + private fun startOutOfProcessMiniNode(config: NodeConfig, vararg extraCmdLineFlag: String): CordaFuture { val debugPort = if (isDebug) debugPortAllocation.nextPort() else null val monitorPort = if (jmxPolicy.startJmxHttpServer) jmxPolicy.jmxHttpServerPortAllocation?.nextPort() else null val process = startOutOfProcessNode( @@ -608,7 +616,7 @@ class DriverDSLImpl( systemProperties, cordappPackages, "200m", - extraCmdLineFlag + *extraCmdLineFlag ) return poll(executorService, "$extraCmdLineFlag (${config.corda.myLegalName})") { @@ -652,7 +660,7 @@ class DriverDSLImpl( } else { val debugPort = if (isDebug) debugPortAllocation.nextPort() else null val monitorPort = if (jmxPolicy.startJmxHttpServer) jmxPolicy.jmxHttpServerPortAllocation?.nextPort() else null - val process = startOutOfProcessNode(config, quasarJarPath, debugPort, jolokiaJarPath, monitorPort, systemProperties, cordappPackages, maximumHeapSize, null) + val process = startOutOfProcessNode(config, quasarJarPath, debugPort, jolokiaJarPath, monitorPort, systemProperties, cordappPackages, maximumHeapSize) if (waitForNodesToFinish) { state.locked { processes += process @@ -763,7 +771,7 @@ class DriverDSLImpl( overriddenSystemProperties: Map, cordappPackages: List, maximumHeapSize: String, - extraCmdLineFlag: String? + vararg extraCmdLineFlag: String ): Process { log.info("Starting out-of-process Node ${config.corda.myLegalName.organisation}, " + "debug port is " + (debugPort ?: "not enabled") + ", " + @@ -801,9 +809,7 @@ class DriverDSLImpl( "--base-directory=${config.corda.baseDirectory}", "--logging-level=$loggingLevel", "--no-local-shell").also { - if (extraCmdLineFlag != null) { - it += extraCmdLineFlag - } + it += extraCmdLineFlag }.toList() return ProcessUtilities.startCordaProcess(