diff --git a/docs/source/running-dev-keystore-generator.rst b/docs/source/running-dev-keystore-generator.rst new file mode 100644 index 0000000000..d729b66635 --- /dev/null +++ b/docs/source/running-dev-keystore-generator.rst @@ -0,0 +1,40 @@ +Running the dev keystore generator +================================== + +The dev keystore generator is a utility tool designed only for internal use. Sometimes our certificates change (e.g. new +extensions are added, some of them are modified...). In order to stay consistent with the rest of the Corda platform and in +particular with Corda node (and its DEV execution mode), we need a facility that would allow us to easily create keystore containing +both root and doorman certificates together with their keys. Those certificates will reflect the most recent state of the Corda certificates. +In addition, a truststore file (containing the root certificate) is also generated. Once generated, those files (i.e. keystore and truststore) +can be copied to an appropriate node directory. + +Although, the output of the tool is strongly bound to the node execution process (i.e. expected key store file name, trust store file name, passwords are hardcoded), +it can be used to generate arbitrary keystore and truststore files with Corda certificates. Therefore, the tool supports a custom configuration. + +Configuration file +------------------ +At startup the dev generator tool reads a configuration file, passed with ``--config-file`` on the command line. + +This is an example of what a generator configuration file might look like: + .. literalinclude:: ../../network-management/dev-generator.conf + +Invoke the tool with ``-?`` for a full list of supported command-line arguments. + +If no configuration file is provided, all the options default to the node expected values. + + +Configuration parameters +------------------------ +Allowed parameters are: + +:privateKeyPass: Password for both Root and Doorman private keys. Default value: "cordacadevkeypass". + +:keyStorePass: Password for the keystore file. Default value: "cordacadevpass". + +:keyStoreFileName: File name for the keystore file. Default value: "cordadevcakeys.jks". + +:trustStorePass: Password for the truststore file. Default value: "trustpass". + +:trustStoreFileName: File name for the truststore file. Default value: "cordatruststore.jks". + +:directory: Directory in which both keystore and trustore files should be created. Default value: "./certificates" \ No newline at end of file diff --git a/network-management/dev-generator.conf b/network-management/dev-generator.conf new file mode 100644 index 0000000000..dbbe1246e4 --- /dev/null +++ b/network-management/dev-generator.conf @@ -0,0 +1,6 @@ +privateKeyPass ="cordacadevkeypass" +keyStorePass = "cordacadevpass" +keyStoreFileName = "cordadevcakeys.jks" +trustStorePass = "trustpass" +trustStoreFileName = "cordatruststore.jks" +directory = "./certificates" diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/dev/Configuration.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/dev/Configuration.kt new file mode 100644 index 0000000000..5996294fbf --- /dev/null +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/dev/Configuration.kt @@ -0,0 +1,63 @@ +package com.r3.corda.networkmanage.dev + +import com.r3.corda.networkmanage.common.utils.ShowHelpException +import com.r3.corda.networkmanage.hsm.generator.CommandLineOptions +import com.typesafe.config.ConfigFactory +import com.typesafe.config.ConfigParseOptions +import joptsimple.OptionParser +import net.corda.nodeapi.internal.* +import net.corda.nodeapi.internal.config.parseAs +import java.io.File +import java.nio.file.Path +import java.nio.file.Paths + +/** + * Holds configuration necessary for generating DEV key store and trust store. + */ +data class GeneratorConfiguration(val privateKeyPass: String = DEV_CA_PRIVATE_KEY_PASS, + val keyStorePass: String = DEV_CA_KEY_STORE_PASS, + val keyStoreFileName: String = DEV_CA_KEY_STORE_FILE, + val trustStorePass: String = DEV_CA_TRUST_STORE_PASS, + val trustStoreFileName: String = DEV_CA_TRUST_STORE_FILE, + val directory: Path = DEFAULT_DIRECTORY) { + companion object { + val DEFAULT_DIRECTORY = File("./certificates").toPath() + } +} + +/** + * Parses dev generator command line options. + */ +fun parseCommandLine(vararg args: String): CommandLineOptions? { + val optionParser = OptionParser() + val configFileArg = optionParser + .accepts("config-file", "The path to the config file") + .withRequiredArg() + .describedAs("filepath") + val helpOption = optionParser.acceptsAll(listOf("h", "?", "help"), "show help").forHelp() + + val optionSet = optionParser.parse(*args) + // Print help and exit on help option. + if (optionSet.has(helpOption)) { + throw ShowHelpException(optionParser) + } + return if (optionSet.has(configFileArg)) { + CommandLineOptions(Paths.get(optionSet.valueOf(configFileArg)).toAbsolutePath()) + } else { + null + } +} + +/** + * Parses a configuration file, which contains all the configuration - i.e. for the key store generator. + */ +fun parseParameters(configFile: Path?): GeneratorConfiguration { + return if (configFile == null) { + GeneratorConfiguration() + } else { + ConfigFactory + .parseFile(configFile.toFile(), ConfigParseOptions.defaults().setAllowMissing(true)) + .resolve() + .parseAs() + } +} \ No newline at end of file diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/dev/Main.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/dev/Main.kt new file mode 100644 index 0000000000..d0833c7e49 --- /dev/null +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/dev/Main.kt @@ -0,0 +1,76 @@ +package com.r3.corda.networkmanage.dev + +import com.r3.corda.networkmanage.doorman.CORDA_X500_BASE +import net.corda.core.crypto.Crypto +import net.corda.core.internal.createDirectories +import net.corda.core.internal.div +import net.corda.nodeapi.internal.crypto.CertificateType +import net.corda.nodeapi.internal.crypto.X509KeyStore +import net.corda.nodeapi.internal.crypto.X509Utilities +import org.apache.logging.log4j.LogManager +import java.io.File +import javax.security.auth.x500.X500Principal +import kotlin.system.exitProcess + +private val logger = LogManager.getLogger("com.r3.corda.networkmanage.dev.Main") + +/** + * This is an internal utility method used to generate a DEV certificate store file containing both root and doorman keys/certificates. + * Additionally, it generates a trust file containing the root certificate. + * + * Note: It can be quickly executed in IntelliJ by right-click on the main method. It will generate the keystore and the trustore + * with settings expected by node running in the dev mode. The files will be generated in the root project directory. + * Look for the 'certificates' directory. + */ +fun main(args: Array) { + run(parseParameters(parseCommandLine(*args)?.configFile)) +} + +fun run(configuration: GeneratorConfiguration) { + configuration.run { + val keyStoreFile = File("$directory/$keyStoreFileName").toPath() + keyStoreFile.parent.createDirectories() + val keyStore = X509KeyStore.fromFile(keyStoreFile, keyStorePass, createNew = true) + + checkCertificateNotInKeyStore(X509Utilities.CORDA_ROOT_CA, keyStore) { exitProcess(1) } + checkCertificateNotInKeyStore(X509Utilities.CORDA_INTERMEDIATE_CA, keyStore) { exitProcess(1) } + + val rootKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val rootCert = X509Utilities.createSelfSignedCACertificate( + X500Principal("CN=Corda Root CA,$CORDA_X500_BASE"), + rootKeyPair) + keyStore.update { + setPrivateKey(X509Utilities.CORDA_ROOT_CA, rootKeyPair.private, listOf(rootCert), privateKeyPass) + } + logger.info("Root CA keypair and certificate stored in ${keyStoreFile.toAbsolutePath()}.") + logger.info(rootCert) + + val trustStorePath = directory / trustStoreFileName + X509KeyStore.fromFile(trustStorePath, trustStorePass, createNew = true).update { + setCertificate(X509Utilities.CORDA_ROOT_CA, rootCert) + } + logger.info("Trust store for distribution to nodes created in $trustStorePath") + + val doormanKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val cert = X509Utilities.createCertificate( + CertificateType.INTERMEDIATE_CA, + rootCert, + rootKeyPair, + X500Principal("CN=Corda Doorman CA,$CORDA_X500_BASE"), + doormanKeyPair.public + ) + keyStore.update { + setPrivateKey(X509Utilities.CORDA_INTERMEDIATE_CA, doormanKeyPair.private, listOf(cert, rootCert), privateKeyPass) + } + logger.info("Doorman CA keypair and certificate stored in ${keyStoreFile.toAbsolutePath()}.") + logger.info(cert) + } +} + +private fun checkCertificateNotInKeyStore(certAlias: String, keyStore: X509KeyStore, onFail: () -> Unit) { + if (certAlias in keyStore) { + logger.info("$certAlias already exists in keystore, process will now terminate.") + logger.info(keyStore.getCertificate(certAlias)) + onFail.invoke() + } +} \ No newline at end of file diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/NetworkManagementUtilities.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/NetworkManagementUtilities.kt index f05536814e..a646f5dcff 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/NetworkManagementUtilities.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/NetworkManagementUtilities.kt @@ -13,7 +13,7 @@ import javax.security.auth.x500.X500Principal import kotlin.system.exitProcess // TODO The cert subjects need to be configurable -private const val CORDA_X500_BASE = "O=R3 HoldCo LLC,OU=Corda,L=New York,C=US" +const val CORDA_X500_BASE = "O=R3 HoldCo LLC,OU=Corda,L=New York,C=US" const val NETWORK_ROOT_TRUSTSTORE_FILENAME = "network-root-truststore.jks" /** Read password from console, do a readLine instead if console is null (e.g. when debugging in IDE). */ diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/generator/Main.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/generator/Main.kt index 28804146c0..3bf40f294b 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/generator/Main.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/generator/Main.kt @@ -8,7 +8,7 @@ import org.apache.logging.log4j.LogManager private val log = LogManager.getLogger("com.r3.corda.networkmanage.hsm.generator.Main") fun main(args: Array) { - run(parseParameters(parseCommandLine(*args).configFile)) + run(parseParameters(parseCommandLine(*args)?.configFile)) } fun run(parameters: GeneratorParameters) { diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/dev/DevGeneratorTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/dev/DevGeneratorTest.kt new file mode 100644 index 0000000000..1ad1e63760 --- /dev/null +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/dev/DevGeneratorTest.kt @@ -0,0 +1,32 @@ +package com.r3.corda.networkmanage.dev + +import net.corda.nodeapi.internal.crypto.X509KeyStore +import net.corda.nodeapi.internal.crypto.X509Utilities +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.File +import kotlin.test.assertTrue + +class DevGeneratorTest { + + @Rule + @JvmField + val tempFolder = TemporaryFolder() + + @Test + fun `key store and trust store are created and contain the certificates`() { + // given + val config = GeneratorConfiguration(directory = tempFolder.root.toPath()) + + // when + run(config) + + // then + val keyStoreFile = File("${config.directory}/${config.keyStoreFileName}").toPath() + val keyStore = X509KeyStore.fromFile(keyStoreFile, config.keyStorePass, createNew = true) + + assertTrue(X509Utilities.CORDA_INTERMEDIATE_CA in keyStore) + assertTrue(X509Utilities.CORDA_ROOT_CA in keyStore) + } +} \ No newline at end of file diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/dev/GeneratorConfigurationTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/dev/GeneratorConfigurationTest.kt new file mode 100644 index 0000000000..c2cb1893f4 --- /dev/null +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/dev/GeneratorConfigurationTest.kt @@ -0,0 +1,31 @@ +package com.r3.corda.networkmanage.dev + +import com.r3.corda.networkmanage.hsm.generator.parseCommandLine +import net.corda.nodeapi.internal.DEV_CA_KEY_STORE_FILE +import net.corda.nodeapi.internal.DEV_CA_KEY_STORE_PASS +import net.corda.nodeapi.internal.DEV_CA_TRUST_STORE_FILE +import net.corda.nodeapi.internal.DEV_CA_TRUST_STORE_PASS +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.File +import kotlin.test.assertEquals + +class GeneratorConfigurationTest { + + @Rule + @JvmField + val tempFolder = TemporaryFolder() + + private val configPath = File("dev-generator.conf").absolutePath + + @Test + fun `config file is parsed correctly`() { + val config = parseParameters(parseCommandLine("--config-file", configPath).configFile) + assertEquals(GeneratorConfiguration.DEFAULT_DIRECTORY, config.directory) + assertEquals(DEV_CA_KEY_STORE_FILE, config.keyStoreFileName) + assertEquals(DEV_CA_KEY_STORE_PASS, config.keyStorePass) + assertEquals(DEV_CA_TRUST_STORE_PASS, config.trustStorePass) + assertEquals(DEV_CA_TRUST_STORE_FILE, config.trustStoreFileName) + } +} \ No newline at end of file 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 70c32ed6cd..96a78107a8 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 @@ -95,14 +95,18 @@ fun createDevNodeCa(intermediateCa: CertificateAndKeyPair, } val DEV_INTERMEDIATE_CA: CertificateAndKeyPair get() = DevCaHelper.loadDevCa(X509Utilities.CORDA_INTERMEDIATE_CA) - val DEV_ROOT_CA: CertificateAndKeyPair get() = DevCaHelper.loadDevCa(X509Utilities.CORDA_ROOT_CA) +val DEV_CA_PRIVATE_KEY_PASS: String = "cordacadevkeypass" +val DEV_CA_KEY_STORE_FILE: String = "cordadevcakeys.jks" +val DEV_CA_KEY_STORE_PASS: String = "cordacadevpass" +val DEV_CA_TRUST_STORE_FILE: String = "cordatruststore.jks" +val DEV_CA_TRUST_STORE_PASS: String = "trustpass" // We need a class so that we can get hold of the class loader internal object DevCaHelper { fun loadDevCa(alias: String): CertificateAndKeyPair { // TODO: Should be identity scheme - val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/cordadevcakeys.jks"), "cordacadevpass") - return caKeyStore.getCertificateAndKeyPair(alias, "cordacadevkeypass") + val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/$DEV_CA_KEY_STORE_FILE"), "$DEV_CA_KEY_STORE_PASS") + return caKeyStore.getCertificateAndKeyPair(alias, "$DEV_CA_PRIVATE_KEY_PASS") } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt index f950ccdc7b..63d8d4fcf5 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt @@ -159,7 +159,7 @@ object X509Utilities { val builder = JcaX509v3CertificateBuilder(issuer, serial, validityWindow.first, validityWindow.second, subject, subjectPublicKey) .addExtension(Extension.subjectKeyIdentifier, false, BcX509ExtensionUtils().createSubjectKeyIdentifier(subjectPublicKeyInfo)) - .addExtension(Extension.basicConstraints, certificateType.isCA, BasicConstraints(certificateType.isCA)) + .addExtension(Extension.basicConstraints, true, BasicConstraints(certificateType.isCA)) .addExtension(Extension.keyUsage, false, certificateType.keyUsage) .addExtension(Extension.extendedKeyUsage, false, keyPurposes) diff --git a/node-api/src/main/resources/certificates/cordadevcakeys.jks b/node-api/src/main/resources/certificates/cordadevcakeys.jks index af5ff8fce8..3dc11f1fc5 100644 Binary files a/node-api/src/main/resources/certificates/cordadevcakeys.jks and b/node-api/src/main/resources/certificates/cordadevcakeys.jks differ diff --git a/node-api/src/main/resources/certificates/cordatruststore.jks b/node-api/src/main/resources/certificates/cordatruststore.jks index dd2c81122c..b91e35b8d5 100644 Binary files a/node-api/src/main/resources/certificates/cordatruststore.jks and b/node-api/src/main/resources/certificates/cordatruststore.jks differ 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 c58d21b359..910e2832ff 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,6 +8,8 @@ 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.DEV_CA_TRUST_STORE_FILE +import net.corda.nodeapi.internal.DEV_CA_TRUST_STORE_PASS import net.corda.nodeapi.internal.config.SSLConfiguration import net.corda.nodeapi.internal.createDevKeyStores import net.corda.nodeapi.internal.crypto.X509KeyStore @@ -55,7 +57,7 @@ fun NodeConfiguration.configureWithDevSSLCertificate() = configureDevKeyAndTrust fun SSLConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500Name) { certificatesDirectory.createDirectories() if (!trustStoreFile.exists()) { - loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/cordatruststore.jks"), "trustpass").save(trustStoreFile, trustStorePassword) + loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/$DEV_CA_TRUST_STORE_FILE"), "$DEV_CA_TRUST_STORE_PASS").save(trustStoreFile, trustStorePassword) } if (!sslKeystore.exists() || !nodeKeystore.exists()) { val (nodeKeyStore) = createDevKeyStores(myLegalName)