diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/DoormanParameters.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/DoormanParameters.kt new file mode 100644 index 0000000000..adee96471f --- /dev/null +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/DoormanParameters.kt @@ -0,0 +1,49 @@ +package com.r3.corda.doorman + +import com.r3.corda.doorman.OptionParserHelper.toConfigWithOptions +import net.corda.core.div +import net.corda.node.services.config.ConfigHelper +import net.corda.node.services.config.getOrElse +import net.corda.node.services.config.getValue +import java.nio.file.Path +import java.nio.file.Paths +import java.util.* + +class DoormanParameters(args: Array) { + private val argConfig = args.toConfigWithOptions { + accepts("basedir", "Overriding configuration filepath, default to current directory.").withRequiredArg().describedAs("filepath") + accepts("keygen", "Generate CA keypair and certificate using provide Root CA key.").withOptionalArg() + accepts("rootKeygen", "Generate Root CA keypair and certificate.").withOptionalArg() + accepts("approveAll", "Approve all certificate signing request.").withOptionalArg() + accepts("keystorePath", "CA keystore filepath, default to [basedir]/certificates/caKeystore.jks.").withRequiredArg().describedAs("filepath") + accepts("rootStorePath", "Root CA keystore filepath, default to [basedir]/certificates/rootCAKeystore.jks.").withRequiredArg().describedAs("filepath") + accepts("keystorePassword", "CA keystore password.").withRequiredArg().describedAs("password") + accepts("caPrivateKeyPassword", "CA private key password.").withRequiredArg().describedAs("password") + accepts("rootKeystorePassword", "Root CA keystore password.").withRequiredArg().describedAs("password") + accepts("rootPrivateKeyPassword", "Root private key password.").withRequiredArg().describedAs("password") + accepts("host", "Doorman web service host override").withRequiredArg().describedAs("hostname") + accepts("port", "Doorman web service port override").withRequiredArg().ofType(Int::class.java).describedAs("port number") + } + private val basedir by argConfig.getOrElse { Paths.get(".") } + private val config = argConfig.withFallback(ConfigHelper.loadConfig(basedir, allowMissingConfig = true)) + val keystorePath: Path by config.getOrElse { basedir / "certificates" / "caKeystore.jks" } + val rootStorePath: Path by config.getOrElse { basedir / "certificates" / "rootCAKeystore.jks" } + val keystorePassword: String? by config.getOrElse { null } + val caPrivateKeyPassword: String? by config.getOrElse { null } + val rootKeystorePassword: String? by config.getOrElse { null } + val rootPrivateKeyPassword: String? by config.getOrElse { null } + val approveAll: Boolean by config.getOrElse { false } + val host: String by config + val port: Int by config + val dataSourceProperties: Properties by config + private val keygen: Boolean by config.getOrElse { false } + private val rootKeygen: Boolean by config.getOrElse { false } + + val mode = if (rootKeygen) Mode.ROOT_KEYGEN else if (keygen) Mode.CA_KEYGEN else Mode.DOORMAN + + enum class Mode { + DOORMAN, CA_KEYGEN, ROOT_KEYGEN + } +} + + diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/Main.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/Main.kt index 5a64a2c916..bd51d30a75 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/Main.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/Main.kt @@ -1,14 +1,24 @@ package com.r3.corda.doorman import com.google.common.net.HostAndPort +import com.r3.corda.doorman.DoormanServer.Companion.logger +import com.r3.corda.doorman.persistence.CertificationRequestStorage import com.r3.corda.doorman.persistence.DBCertificateRequestStorage -import joptsimple.ArgumentAcceptingOptionSpec -import joptsimple.OptionParser +import net.corda.core.createDirectories import net.corda.core.crypto.X509Utilities -import net.corda.core.utilities.debug +import net.corda.core.crypto.X509Utilities.CACertAndKey +import net.corda.core.crypto.X509Utilities.CORDA_INTERMEDIATE_CA +import net.corda.core.crypto.X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY +import net.corda.core.crypto.X509Utilities.CORDA_ROOT_CA +import net.corda.core.crypto.X509Utilities.CORDA_ROOT_CA_PRIVATE_KEY +import net.corda.core.crypto.X509Utilities.addOrReplaceKey +import net.corda.core.crypto.X509Utilities.createIntermediateCert +import net.corda.core.crypto.X509Utilities.loadCertificateAndKey +import net.corda.core.crypto.X509Utilities.loadKeyStore +import net.corda.core.crypto.X509Utilities.loadOrCreateKeyStore +import net.corda.core.crypto.X509Utilities.saveKeyStore +import net.corda.core.seconds import net.corda.core.utilities.loggerFor -import net.corda.node.services.config.ConfigHelper -import net.corda.node.services.config.getProperties import net.corda.node.utilities.configureDatabase import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest import org.eclipse.jetty.server.Server @@ -19,22 +29,29 @@ import org.eclipse.jetty.servlet.ServletHolder import org.glassfish.jersey.server.ResourceConfig import org.glassfish.jersey.servlet.ServletContainer import java.io.Closeable +import java.lang.Thread.sleep import java.net.InetSocketAddress -import java.nio.file.Paths +import java.security.cert.Certificate import kotlin.concurrent.thread import kotlin.system.exitProcess /** - * CertificateSigningServer runs on Jetty server and provide certificate signing service via http. + * DoormanServer runs on Jetty server and provide certificate signing service via http. * The server will require keystorePath, keystore password and key password via command line input. * The Intermediate CA certificate,Intermediate CA private key and Root CA Certificate should use alias name specified in [X509Utilities] */ -class DoormanServer(val webServerAddr: HostAndPort, val doormanWebService: DoormanWebService) : Closeable { + +class DoormanServer(webServerAddr: HostAndPort, val caCertAndKey: CACertAndKey, val rootCACert: Certificate, val storage: CertificationRequestStorage) : Closeable { companion object { - val log = loggerFor() + val logger = loggerFor() + } + + private val server: Server = Server(InetSocketAddress(webServerAddr.hostText, webServerAddr.port)).apply { + handler = HandlerCollection().apply { + addHandler(buildServletContextHandler()) + } } - private val server: Server = initWebServer() val hostAndPort: HostAndPort get() = server.connectors .map { it as? ServerConnector } .filterNotNull() @@ -42,20 +59,15 @@ class DoormanServer(val webServerAddr: HostAndPort, val doormanWebService: Doorm .first() override fun close() { - log.info("Shutting down CertificateSigningService...") + logger.info("Shutting down Doorman Web Services...") server.stop() server.join() } - private fun initWebServer(): Server { - return Server(InetSocketAddress(webServerAddr.hostText, webServerAddr.port)).apply { - log.info("Starting CertificateSigningService...") - handler = HandlerCollection().apply { - addHandler(buildServletContextHandler()) - } - start() - log.info("CertificateSigningService started on $hostAndPort") - } + fun start() { + logger.info("Starting Doorman Web Services...") + server.start() + logger.info("Doorman Web Services started on $hostAndPort") } private fun buildServletContextHandler(): ServletContextHandler { @@ -63,7 +75,7 @@ class DoormanServer(val webServerAddr: HostAndPort, val doormanWebService: Doorm contextPath = "/" val resourceConfig = ResourceConfig().apply { // Add your API provider classes (annotated for JAX-RS) here - register(doormanWebService) + register(DoormanWebService(caCertAndKey, rootCACert, storage)) } val jerseyServlet = ServletHolder(ServletContainer(resourceConfig)).apply { initOrder = 0 // Initialise at server start @@ -73,62 +85,115 @@ class DoormanServer(val webServerAddr: HostAndPort, val doormanWebService: Doorm } } -object ParamsSpec { - val parser = OptionParser() - val basedir: ArgumentAcceptingOptionSpec? = parser.accepts("basedir", "Overriding configuration file path.") - .withRequiredArg() +/** Read password from console, do a readLine instead if console is null (e.g. when debugging in IDE). */ +private fun readPassword(fmt: String): String { + return if (System.console() != null) { + String(System.console().readPassword(fmt)) + } else { + print(fmt) + readLine()!! + } +} + +private fun DoormanParameters.generateRootKeyPair() { + println("Generating Root CA keypair and certificate.") + // Get password from console if not in config. + val rootKeystorePassword = rootKeystorePassword ?: readPassword("Root Keystore Password: ") + // Ensure folder exists. + rootStorePath.parent.createDirectories() + val rootStore = loadOrCreateKeyStore(rootStorePath, rootKeystorePassword) + val rootPrivateKeyPassword = rootPrivateKeyPassword ?: readPassword("Root Private Key Password: ") + + if (rootStore.containsAlias(CORDA_ROOT_CA_PRIVATE_KEY)) { + val oldKey = loadOrCreateKeyStore(rootStorePath, rootKeystorePassword).getCertificate(CORDA_ROOT_CA_PRIVATE_KEY).publicKey + println("Key $CORDA_ROOT_CA_PRIVATE_KEY already exists in keystore, process will now terminate.") + println(oldKey) + exitProcess(1) + } + + val selfSignCert = X509Utilities.createSelfSignedCACert(CORDA_ROOT_CA) + rootStore.addOrReplaceKey(CORDA_ROOT_CA_PRIVATE_KEY, selfSignCert.keyPair.private, rootPrivateKeyPassword.toCharArray(), arrayOf(selfSignCert.certificate)) + saveKeyStore(rootStore, rootStorePath, rootKeystorePassword) + + println("Root CA keypair and certificate stored in $rootStorePath.") + println(loadKeyStore(rootStorePath, rootKeystorePassword).getCertificate(CORDA_ROOT_CA_PRIVATE_KEY).publicKey) +} + +private fun DoormanParameters.generateCAKeyPair() { + println("Generating Intermediate CA keypair and certificate using root keystore $rootStorePath.") + // Get password from console if not in config. + val rootKeystorePassword = rootKeystorePassword ?: readPassword("Root Keystore Password: ") + val rootPrivateKeyPassword = rootPrivateKeyPassword ?: readPassword("Root Private Key Password: ") + val rootKeyStore = loadKeyStore(rootStorePath, rootKeystorePassword) + + val rootKeyAndCert = loadCertificateAndKey(rootKeyStore, rootPrivateKeyPassword, CORDA_ROOT_CA_PRIVATE_KEY) + + val keystorePassword = keystorePassword ?: readPassword("Keystore Password: ") + val caPrivateKeyPassword = caPrivateKeyPassword ?: readPassword("CA Private Key Password: ") + // Ensure folder exists. + keystorePath.parent.createDirectories() + val keyStore = loadOrCreateKeyStore(keystorePath, keystorePassword) + + if (keyStore.containsAlias(CORDA_INTERMEDIATE_CA_PRIVATE_KEY)) { + val oldKey = loadOrCreateKeyStore(keystorePath, rootKeystorePassword).getCertificate(CORDA_INTERMEDIATE_CA_PRIVATE_KEY).publicKey + println("Key $CORDA_INTERMEDIATE_CA_PRIVATE_KEY already exists in keystore, process will now terminate.") + println(oldKey) + exitProcess(1) + } + + val intermediateKeyAndCert = createIntermediateCert(CORDA_INTERMEDIATE_CA, rootKeyAndCert) + keyStore.addOrReplaceKey(CORDA_INTERMEDIATE_CA_PRIVATE_KEY, intermediateKeyAndCert.keyPair.private, + caPrivateKeyPassword.toCharArray(), arrayOf(intermediateKeyAndCert.certificate, rootKeyAndCert.certificate)) + saveKeyStore(keyStore, keystorePath, keystorePassword) + println("Intermediate CA keypair and certificate stored in $keystorePath.") + println(loadKeyStore(keystorePath, keystorePassword).getCertificate(CORDA_INTERMEDIATE_CA_PRIVATE_KEY).publicKey) +} + +private fun DoormanParameters.startDoorman() { + logger.info("Starting Doorman server.") + // Get password from console if not in config. + val keystorePassword = keystorePassword ?: readPassword("Keystore Password: ") + val caPrivateKeyPassword = caPrivateKeyPassword ?: readPassword("CA Private Key Password: ") + val keystore = X509Utilities.loadKeyStore(keystorePath, keystorePassword) + val rootCACert = keystore.getCertificateChain(CORDA_INTERMEDIATE_CA_PRIVATE_KEY).last() + val caCertAndKey = X509Utilities.loadCertificateAndKey(keystore, caPrivateKeyPassword, CORDA_INTERMEDIATE_CA_PRIVATE_KEY) + // Create DB connection. + val (datasource, database) = configureDatabase(dataSourceProperties) + val storage = DBCertificateRequestStorage(database) + // Daemon thread approving all request periodically. + if (approveAll) { + thread(name = "Request Approval Daemon", isDaemon = true) { + logger.warn("Doorman server is in 'Approve All' mode, this will approve all incoming certificate signing request.") + while (true) { + sleep(10.seconds.toMillis()) + for (id in storage.getPendingRequestIds()) { + storage.approveRequest(id, { + JcaPKCS10CertificationRequest(it.request).run { + X509Utilities.createServerCert(subject, publicKey, caCertAndKey, + if (it.ipAddress == it.hostName) listOf() else listOf(it.hostName), listOf(it.ipAddress)) + } + }) + logger.info("Approved request $id") + } + } + } + } + val doorman = DoormanServer(HostAndPort.fromParts(host, port), caCertAndKey, rootCACert, storage) + doorman.start() + Runtime.getRuntime().addShutdownHook(thread(start = false) { doorman.close() }) } fun main(args: Array) { - val log = DoormanServer.log - log.info("Starting certificate signing server.") try { - ParamsSpec.parser.parse(*args) - } catch (ex: Exception) { - log.error("Unable to parse args", ex) - ParamsSpec.parser.printHelpOn(System.out) - exitProcess(1) - }.run { - val basedir = Paths.get(valueOf(ParamsSpec.basedir) ?: ".") - val config = ConfigHelper.loadConfig(basedir) - - val keystore = X509Utilities.loadKeyStore(Paths.get(config.getString("keystorePath")).normalize(), config.getString("keyStorePassword")) - val intermediateCACertAndKey = X509Utilities.loadCertificateAndKey(keystore, config.getString("caKeyPassword"), X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY) - val rootCA = keystore.getCertificateChain(X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY).last() - - // Create DB connection. - val (datasource, database) = configureDatabase(config.getProperties("dataSourceProperties")) - val storage = DBCertificateRequestStorage(database) - val service = DoormanWebService(intermediateCACertAndKey, rootCA, storage) - - // Background thread approving all request periodically. - var stopSigner = false - val certSinger = if (config.getBoolean("approveAll")) { - thread { - while (!stopSigner) { - Thread.sleep(1000) - for (id in storage.getPendingRequestIds()) { - storage.approveRequest(id, { - JcaPKCS10CertificationRequest(it.request).run { - X509Utilities.createServerCert(subject, publicKey, intermediateCACertAndKey, - if (it.ipAddress == it.hostName) listOf() else listOf(it.hostName), listOf(it.ipAddress)) - } - }) - log.debug { "Approved $id" } - } - } - log.debug { "Certificate Signer thread stopped." } + // TODO : Remove config overrides and solely use config file after testnet is finalized. + DoormanParameters(args).run { + when (mode) { + DoormanParameters.Mode.ROOT_KEYGEN -> generateRootKeyPair() + DoormanParameters.Mode.CA_KEYGEN -> generateCAKeyPair() + DoormanParameters.Mode.DOORMAN -> startDoorman() } - } else { - null - } - - DoormanServer(HostAndPort.fromParts(config.getString("host"), config.getInt("port")), service).use { - Runtime.getRuntime().addShutdownHook(thread(false) { - stopSigner = true - certSinger?.join() - datasource.close() - }) } + } catch (e: ShowHelpException) { + e.parser.printHelpOn(System.out) } -} \ No newline at end of file +} diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/OptionParserUtilities.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/OptionParserUtilities.kt new file mode 100644 index 0000000000..54847f9407 --- /dev/null +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/OptionParserUtilities.kt @@ -0,0 +1,29 @@ +package com.r3.corda.doorman + +import com.typesafe.config.Config +import com.typesafe.config.ConfigFactory +import joptsimple.ArgumentAcceptingOptionSpec +import joptsimple.OptionParser + +/** + * Convert commandline arguments to [Config] object will allow us to use kotlin delegate with [ConfigHelper]. + */ +object OptionParserHelper { + fun Array.toConfigWithOptions(registerOptions: OptionParser.() -> Unit): Config { + val parser = OptionParser() + val helpOption = parser.acceptsAll(listOf("h", "?", "help"), "show help").forHelp(); + registerOptions(parser) + val optionSet = parser.parse(*this) + // Print help and exit on help option. + if (optionSet.has(helpOption)) { + throw ShowHelpException(parser) + } + // Convert all command line options to Config. + return ConfigFactory.parseMap(parser.recognizedOptions().mapValues { + val optionSpec = it.value + if (optionSpec is ArgumentAcceptingOptionSpec<*> && !optionSpec.requiresArgument() && optionSet.has(optionSpec)) true else optionSpec.value(optionSet) + }.filterValues { it != null }) + } +} + +class ShowHelpException(val parser: OptionParser) : Exception() diff --git a/doorman/src/main/resources/reference.conf b/doorman/src/main/resources/reference.conf index d0629fc739..446362dba0 100644 --- a/doorman/src/main/resources/reference.conf +++ b/doorman/src/main/resources/reference.conf @@ -1,8 +1,8 @@ host = localhost port = 0 -keystorePath = ${basedir}"/certificates/keystore.jks" -keyStorePassword = "password" -caKeyPassword = "password" +keystorePath = ${basedir}"/certificates/caKeystore.jks" +keystorePassword = "password" +caPrivateKeyPassword = "password" approveAll = true dataSourceProperties { diff --git a/doorman/src/test/kotlin/com/r3/corda/doorman/DoormanParametersTest.kt b/doorman/src/test/kotlin/com/r3/corda/doorman/DoormanParametersTest.kt new file mode 100644 index 0000000000..7767d010c0 --- /dev/null +++ b/doorman/src/test/kotlin/com/r3/corda/doorman/DoormanParametersTest.kt @@ -0,0 +1,21 @@ +package com.r3.corda.doorman + +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class DoormanParametersTest { + @Test + fun `parse arg correctly`() { + val params = DoormanParameters(arrayOf("--keygen", "--keystorePath", "./testDummyPath.jks", "--approveAll")) + assertEquals(DoormanParameters.Mode.CA_KEYGEN, params.mode) + assertEquals("./testDummyPath.jks", params.keystorePath.toString()) + assertEquals(0, params.port) + assertTrue(params.approveAll) + + val params2 = DoormanParameters(arrayOf("--keystorePath", "./testDummyPath.jks", "--port", "1000")) + assertEquals(DoormanParameters.Mode.DOORMAN, params2.mode) + assertEquals("./testDummyPath.jks", params2.keystorePath.toString()) + assertEquals(1000, params2.port) + } +} diff --git a/doorman/src/test/kotlin/com/r3/corda/doorman/DoormanServiceTest.kt b/doorman/src/test/kotlin/com/r3/corda/doorman/DoormanServiceTest.kt index 21c42e1afd..6ad0fb07d4 100644 --- a/doorman/src/test/kotlin/com/r3/corda/doorman/DoormanServiceTest.kt +++ b/doorman/src/test/kotlin/com/r3/corda/doorman/DoormanServiceTest.kt @@ -31,7 +31,8 @@ class DoormanServiceTest { private lateinit var doormanServer: DoormanServer private fun startSigningServer(storage: CertificationRequestStorage) { - doormanServer = DoormanServer(HostAndPort.fromParts("localhost", 0), DoormanWebService(intermediateCA, rootCA.certificate, storage)) + doormanServer = DoormanServer(HostAndPort.fromParts("localhost", 0), intermediateCA, rootCA.certificate, storage) + doormanServer.start() } @After