Making BasicConstraints a critical extension (#420)

Path length will be decided in another ticket - https://r3-cev.atlassian.net/browse/ENT-1508
This commit is contained in:
Michal Kit 2018-02-08 14:30:20 +00:00 committed by GitHub
parent 453029e548
commit 8c5f0ac0ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 261 additions and 7 deletions

View File

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

View File

@ -0,0 +1,6 @@
privateKeyPass ="cordacadevkeypass"
keyStorePass = "cordacadevpass"
keyStoreFileName = "cordadevcakeys.jks"
trustStorePass = "trustpass"
trustStoreFileName = "cordatruststore.jks"
directory = "./certificates"

View File

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

View File

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

View File

@ -13,7 +13,7 @@ import javax.security.auth.x500.X500Principal
import kotlin.system.exitProcess import kotlin.system.exitProcess
// TODO The cert subjects need to be configurable // 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" 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). */ /** Read password from console, do a readLine instead if console is null (e.g. when debugging in IDE). */

View File

@ -8,7 +8,7 @@ import org.apache.logging.log4j.LogManager
private val log = LogManager.getLogger("com.r3.corda.networkmanage.hsm.generator.Main") private val log = LogManager.getLogger("com.r3.corda.networkmanage.hsm.generator.Main")
fun main(args: Array<String>) { fun main(args: Array<String>) {
run(parseParameters(parseCommandLine(*args).configFile)) run(parseParameters(parseCommandLine(*args)?.configFile))
} }
fun run(parameters: GeneratorParameters) { fun run(parameters: GeneratorParameters) {

View File

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

View File

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

View File

@ -95,14 +95,18 @@ fun createDevNodeCa(intermediateCa: CertificateAndKeyPair,
} }
val DEV_INTERMEDIATE_CA: CertificateAndKeyPair get() = DevCaHelper.loadDevCa(X509Utilities.CORDA_INTERMEDIATE_CA) 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_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 // We need a class so that we can get hold of the class loader
internal object DevCaHelper { internal object DevCaHelper {
fun loadDevCa(alias: String): CertificateAndKeyPair { fun loadDevCa(alias: String): CertificateAndKeyPair {
// TODO: Should be identity scheme // TODO: Should be identity scheme
val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/cordadevcakeys.jks"), "cordacadevpass") val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/$DEV_CA_KEY_STORE_FILE"), "$DEV_CA_KEY_STORE_PASS")
return caKeyStore.getCertificateAndKeyPair(alias, "cordacadevkeypass") return caKeyStore.getCertificateAndKeyPair(alias, "$DEV_CA_PRIVATE_KEY_PASS")
} }
} }

View File

@ -159,7 +159,7 @@ object X509Utilities {
val builder = JcaX509v3CertificateBuilder(issuer, serial, validityWindow.first, validityWindow.second, subject, subjectPublicKey) val builder = JcaX509v3CertificateBuilder(issuer, serial, validityWindow.first, validityWindow.second, subject, subjectPublicKey)
.addExtension(Extension.subjectKeyIdentifier, false, BcX509ExtensionUtils().createSubjectKeyIdentifier(subjectPublicKeyInfo)) .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.keyUsage, false, certificateType.keyUsage)
.addExtension(Extension.extendedKeyUsage, false, keyPurposes) .addExtension(Extension.extendedKeyUsage, false, keyPurposes)

View File

@ -8,6 +8,8 @@ import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.createDirectories import net.corda.core.internal.createDirectories
import net.corda.core.internal.div import net.corda.core.internal.div
import net.corda.core.internal.exists 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.config.SSLConfiguration
import net.corda.nodeapi.internal.createDevKeyStores import net.corda.nodeapi.internal.createDevKeyStores
import net.corda.nodeapi.internal.crypto.X509KeyStore import net.corda.nodeapi.internal.crypto.X509KeyStore
@ -55,7 +57,7 @@ fun NodeConfiguration.configureWithDevSSLCertificate() = configureDevKeyAndTrust
fun SSLConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500Name) { fun SSLConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500Name) {
certificatesDirectory.createDirectories() certificatesDirectory.createDirectories()
if (!trustStoreFile.exists()) { 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()) { if (!sslKeystore.exists() || !nodeKeystore.exists()) {
val (nodeKeyStore) = createDevKeyStores(myLegalName) val (nodeKeyStore) = createDevKeyStores(myLegalName)