Add cmdline option for network root truststore and password (#2407)

* add cmdline option for network root truststore and password, instead of using node's truststore configuration to avoid confusion.

* revert line auto format

* fix failing integration test

* address PR issue
This commit is contained in:
Patrick Kuo 2018-01-29 13:43:16 +00:00 committed by GitHub
parent 4851d9ca6a
commit 93054a9590
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 90 additions and 66 deletions

View File

@ -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")

View File

@ -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
}

View File

@ -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<String>) {
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<String>) {
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<String>) {
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()

View File

@ -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)

View File

@ -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

View File

@ -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<AbstractNodeConfiguration>().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)
}
}

View File

@ -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 }
}
}
@ -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<Unit> {
private fun startOutOfProcessMiniNode(config: NodeConfig, vararg extraCmdLineFlag: String): CordaFuture<Unit> {
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<String, String>,
cordappPackages: List<String>,
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
}
}.toList()
return ProcessUtilities.startCordaProcess(