From 011dee09d8356c5c86a351643a828492b534884c Mon Sep 17 00:00:00 2001 From: Patrick Kuo Date: Wed, 8 Feb 2017 17:26:33 +0000 Subject: [PATCH 01/10] added keygen functionality to doorman --- .../main/kotlin/com/r3/corda/doorman/Main.kt | 227 ++++++++++++------ .../r3/corda/doorman/OptionParserUtilities.kt | 25 ++ doorman/src/main/resources/reference.conf | 6 +- .../r3/corda/doorman/DoormanParametersTest.kt | 23 ++ .../r3/corda/doorman/DoormanServiceTest.kt | 3 +- 5 files changed, 213 insertions(+), 71 deletions(-) create mode 100644 doorman/src/main/kotlin/com/r3/corda/doorman/OptionParserUtilities.kt create mode 100644 doorman/src/test/kotlin/com/r3/corda/doorman/DoormanParametersTest.kt 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..0afad2e006 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,27 @@ package com.r3.corda.doorman import com.google.common.net.HostAndPort +import com.r3.corda.doorman.OptionParserHelper.toConfigWithOptions +import com.r3.corda.doorman.persistence.CertificationRequestStorage import com.r3.corda.doorman.persistence.DBCertificateRequestStorage -import joptsimple.ArgumentAcceptingOptionSpec -import joptsimple.OptionParser 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.div +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.services.config.getOrElse +import net.corda.node.services.config.getValue import net.corda.node.utilities.configureDatabase import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest import org.eclipse.jetty.server.Server @@ -19,8 +32,12 @@ 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.Path import java.nio.file.Paths +import java.security.cert.Certificate +import java.util.* import kotlin.concurrent.thread import kotlin.system.exitProcess @@ -29,12 +46,17 @@ import kotlin.system.exitProcess * 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() } - private val server: Server = initWebServer() + val server: Server = Server(InetSocketAddress(webServerAddr.hostText, webServerAddr.port)).apply { + server.handler = HandlerCollection().apply { + addHandler(buildServletContextHandler()) + } + } + val hostAndPort: HostAndPort get() = server.connectors .map { it as? ServerConnector } .filterNotNull() @@ -42,20 +64,15 @@ class DoormanServer(val webServerAddr: HostAndPort, val doormanWebService: Doorm .first() override fun close() { - log.info("Shutting down CertificateSigningService...") + log.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() { + log.info("Starting Doorman Web Services...") + server.start() + log.info("Doorman Web Services started on $hostAndPort") } private fun buildServletContextHandler(): ServletContextHandler { @@ -63,7 +80,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 +90,138 @@ class DoormanServer(val webServerAddr: HostAndPort, val doormanWebService: Doorm } } -object ParamsSpec { - val parser = OptionParser() - val basedir: ArgumentAcceptingOptionSpec? = parser.accepts("basedir", "Overriding configuration file path.") - .withRequiredArg() +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 + 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 + } } 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." } - } + fun readPassword(fmt: String): String { + return if (System.console() != null) { + String(System.console().readPassword(fmt)) } else { - null + print(fmt) + readLine()!! } + } + DoormanParameters(args).run { + val log = DoormanServer.log + when (mode) { + DoormanParameters.Mode.ROOT_KEYGEN -> { + println("Generating Root CA keypair and certificate.") + val rootKeystorePassword = rootKeystorePassword ?: readPassword("Root Keystore Password : ") + val rootStore = loadOrCreateKeyStore(rootStorePath, rootKeystorePassword) + val rootPrivateKeyPassword = rootPrivateKeyPassword ?: readPassword("Root Private Key Password : ") - DoormanServer(HostAndPort.fromParts(config.getString("host"), config.getInt("port")), service).use { - Runtime.getRuntime().addShutdownHook(thread(false) { - stopSigner = true - certSinger?.join() - datasource.close() - }) + 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) + } + DoormanParameters.Mode.CA_KEYGEN -> { + println("Generating Intermediate CA keypair and certificate.") + 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 : ") + 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) + } + DoormanParameters.Mode.DOORMAN -> { + log.info("Starting certificate signing server.") + + 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. + val approvalThread = if (approveAll) { + thread(name = "Request Approval Daemon") { + 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)) + } + }) + log.info("Approved $id") + } + } + } + } else null + DoormanServer(HostAndPort.fromParts(host, port), caCertAndKey, rootCACert, storage).use { + it.start() + it.server.join() + approvalThread?.interrupt() + approvalThread?.join() + } + } } } } \ 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..a90122a1cb --- /dev/null +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/OptionParserUtilities.kt @@ -0,0 +1,25 @@ +package com.r3.corda.doorman + +import com.typesafe.config.Config +import com.typesafe.config.ConfigFactory +import joptsimple.ArgumentAcceptingOptionSpec +import joptsimple.OptionParser +import kotlin.system.exitProcess + +object OptionParserHelper { + fun Array.toConfigWithOptions(options: OptionParser.() -> Unit): Config { + val parser = OptionParser() + val helpOption = parser.acceptsAll(listOf("h", "?", "help"), "show help").forHelp(); + options(parser) + val optionSet = parser.parse(*this) + if (optionSet.has(helpOption)) { + parser.printHelpOn(System.out) + exitProcess(0) + } + + 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 }) + } +} \ No newline at end of file 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..40fb2f3cfc --- /dev/null +++ b/doorman/src/test/kotlin/com/r3/corda/doorman/DoormanParametersTest.kt @@ -0,0 +1,23 @@ +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 From b651074523dc00eabc7431b324c8e1f2c1d059ad Mon Sep 17 00:00:00 2001 From: Patrick Kuo Date: Thu, 9 Feb 2017 10:41:27 +0000 Subject: [PATCH 02/10] some refactoring and more comments. --- .../main/kotlin/com/r3/corda/doorman/Main.kt | 72 ++++++++++--------- .../r3/corda/doorman/OptionParserUtilities.kt | 10 ++- 2 files changed, 44 insertions(+), 38 deletions(-) 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 0afad2e006..c65ff09ce0 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/Main.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/Main.kt @@ -4,6 +4,7 @@ import com.google.common.net.HostAndPort import com.r3.corda.doorman.OptionParserHelper.toConfigWithOptions import com.r3.corda.doorman.persistence.CertificationRequestStorage import com.r3.corda.doorman.persistence.DBCertificateRequestStorage +import net.corda.core.createDirectories import net.corda.core.crypto.X509Utilities import net.corda.core.crypto.X509Utilities.CACertAndKey import net.corda.core.crypto.X509Utilities.CORDA_INTERMEDIATE_CA @@ -42,16 +43,14 @@ 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(webServerAddr: HostAndPort, val caCertAndKey: CACertAndKey, val rootCACert: Certificate, val storage: CertificationRequestStorage) : Closeable { - companion object { - val log = loggerFor() - } +val logger = loggerFor() - val server: Server = Server(InetSocketAddress(webServerAddr.hostText, webServerAddr.port)).apply { +class DoormanServer(webServerAddr: HostAndPort, val caCertAndKey: CACertAndKey, val rootCACert: Certificate, val storage: CertificationRequestStorage) : Closeable { + private val server: Server = Server(InetSocketAddress(webServerAddr.hostText, webServerAddr.port)).apply { server.handler = HandlerCollection().apply { addHandler(buildServletContextHandler()) } @@ -64,15 +63,15 @@ class DoormanServer(webServerAddr: HostAndPort, val caCertAndKey: CACertAndKey, .first() override fun close() { - log.info("Shutting down Doorman Web Services...") + logger.info("Shutting down Doorman Web Services...") server.stop() server.join() } fun start() { - log.info("Starting Doorman Web Services...") + logger.info("Starting Doorman Web Services...") server.start() - log.info("Doorman Web Services started on $hostAndPort") + logger.info("Doorman Web Services started on $hostAndPort") } private fun buildServletContextHandler(): ServletContextHandler { @@ -90,7 +89,7 @@ class DoormanServer(webServerAddr: HostAndPort, val caCertAndKey: CACertAndKey, } } -class DoormanParameters(args: Array) { +private 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() @@ -113,7 +112,7 @@ class DoormanParameters(args: Array) { 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 + val approveAll: Boolean by config.getOrElse { false } val host: String by config val port: Int by config val dataSourceProperties: Properties by config @@ -127,21 +126,25 @@ class DoormanParameters(args: Array) { } } -fun main(args: Array) { - fun readPassword(fmt: String): String { - return if (System.console() != null) { - String(System.console().readPassword(fmt)) - } else { - print(fmt) - readLine()!! - } +/** 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()!! } +} + +fun main(args: Array) { DoormanParameters(args).run { - val log = DoormanServer.log when (mode) { DoormanParameters.Mode.ROOT_KEYGEN -> { 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 : ") @@ -160,7 +163,8 @@ fun main(args: Array) { println(loadKeyStore(rootStorePath, rootKeystorePassword).getCertificate(CORDA_ROOT_CA_PRIVATE_KEY).publicKey) } DoormanParameters.Mode.CA_KEYGEN -> { - println("Generating Intermediate CA keypair and certificate.") + 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) @@ -169,6 +173,8 @@ fun main(args: Array) { 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)) { @@ -186,21 +192,20 @@ fun main(args: Array) { println(loadKeyStore(keystorePath, keystorePassword).getCertificate(CORDA_INTERMEDIATE_CA_PRIVATE_KEY).publicKey) } DoormanParameters.Mode.DOORMAN -> { - log.info("Starting certificate signing server.") - + 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. - val approvalThread = if (approveAll) { - thread(name = "Request Approval Daemon") { + 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()) { @@ -210,18 +215,15 @@ fun main(args: Array) { if (it.ipAddress == it.hostName) listOf() else listOf(it.hostName), listOf(it.ipAddress)) } }) - log.info("Approved $id") + logger.info("Approved request $id") } } } - } else null - DoormanServer(HostAndPort.fromParts(host, port), caCertAndKey, rootCACert, storage).use { - it.start() - it.server.join() - approvalThread?.interrupt() - approvalThread?.join() } + val doorman = DoormanServer(HostAndPort.fromParts(host, port), caCertAndKey, rootCACert, storage) + doorman.start() + Runtime.getRuntime().addShutdownHook(thread(start = false) { doorman.close() }) } } } -} \ 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 index a90122a1cb..b88c32dcdc 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/OptionParserUtilities.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/OptionParserUtilities.kt @@ -6,17 +6,21 @@ import joptsimple.ArgumentAcceptingOptionSpec import joptsimple.OptionParser import kotlin.system.exitProcess +/** + * Convert commandline arguments to [Config] object will allow us to use kotlin delegate with [ConfigHelper]. + */ object OptionParserHelper { - fun Array.toConfigWithOptions(options: OptionParser.() -> Unit): Config { + fun Array.toConfigWithOptions(registerOptions: OptionParser.() -> Unit): Config { val parser = OptionParser() val helpOption = parser.acceptsAll(listOf("h", "?", "help"), "show help").forHelp(); - options(parser) + registerOptions(parser) val optionSet = parser.parse(*this) + // Print help and exit on help option. if (optionSet.has(helpOption)) { parser.printHelpOn(System.out) exitProcess(0) } - + // 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) From 94bc130721e5eb0dcd8e5815ac87aa0bd8d4c974 Mon Sep 17 00:00:00 2001 From: Patrick Kuo Date: Fri, 10 Feb 2017 10:28:27 +0000 Subject: [PATCH 03/10] Addressed PR issues --- .../main/kotlin/com/r3/corda/doorman/Main.kt | 170 +++++++++--------- .../r3/corda/doorman/DoormanParametersTest.kt | 2 - 2 files changed, 90 insertions(+), 82 deletions(-) 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 c65ff09ce0..83ab2337cf 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/Main.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/Main.kt @@ -1,6 +1,7 @@ package com.r3.corda.doorman import com.google.common.net.HostAndPort +import com.r3.corda.doorman.DoormanServer.Companion.logger import com.r3.corda.doorman.OptionParserHelper.toConfigWithOptions import com.r3.corda.doorman.persistence.CertificationRequestStorage import com.r3.corda.doorman.persistence.DBCertificateRequestStorage @@ -47,9 +48,12 @@ import kotlin.system.exitProcess * 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] */ -val logger = loggerFor() class DoormanServer(webServerAddr: HostAndPort, val caCertAndKey: CACertAndKey, val rootCACert: Certificate, val storage: CertificationRequestStorage) : Closeable { + companion object { + val logger = loggerFor() + } + private val server: Server = Server(InetSocketAddress(webServerAddr.hostText, webServerAddr.port)).apply { server.handler = HandlerCollection().apply { addHandler(buildServletContextHandler()) @@ -89,7 +93,7 @@ class DoormanServer(webServerAddr: HostAndPort, val caCertAndKey: CACertAndKey, } } -private class DoormanParameters(args: Array) { +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() @@ -136,94 +140,100 @@ private fun readPassword(fmt: String): String { } } -fun main(args: Array) { - DoormanParameters(args).run { - when (mode) { - DoormanParameters.Mode.ROOT_KEYGEN -> { - 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 : ") +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) - } + 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) + 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) - } - DoormanParameters.Mode.CA_KEYGEN -> { - 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) + println("Root CA keypair and certificate stored in $rootStorePath.") + println(loadKeyStore(rootStorePath, rootKeystorePassword).getCertificate(CORDA_ROOT_CA_PRIVATE_KEY).publicKey) +} - val rootKeyAndCert = loadCertificateAndKey(rootKeyStore, rootPrivateKeyPassword, CORDA_ROOT_CA_PRIVATE_KEY) +private fun DoormanParameters.generateKeyPair() { + 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 keystorePassword = keystorePassword ?: readPassword("Keystore Password : ") - val caPrivateKeyPassword = caPrivateKeyPassword ?: readPassword("CA Private Key Password : ") - // Ensure folder exists. - keystorePath.parent.createDirectories() - val keyStore = loadOrCreateKeyStore(keystorePath, keystorePassword) + val rootKeyAndCert = loadCertificateAndKey(rootKeyStore, rootPrivateKeyPassword, CORDA_ROOT_CA_PRIVATE_KEY) - 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 keystorePassword = keystorePassword ?: readPassword("Keystore Password : ") + val caPrivateKeyPassword = caPrivateKeyPassword ?: readPassword("CA Private Key Password : ") + // Ensure folder exists. + keystorePath.parent.createDirectories() + val keyStore = loadOrCreateKeyStore(keystorePath, keystorePassword) - 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) - } - DoormanParameters.Mode.DOORMAN -> { - 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") - } + 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() }) } } } + val doorman = DoormanServer(HostAndPort.fromParts(host, port), caCertAndKey, rootCACert, storage) + doorman.start() + Runtime.getRuntime().addShutdownHook(thread(start = false) { doorman.close() }) +} + +fun main(args: Array) { + DoormanParameters(args).run { + when (mode) { + DoormanParameters.Mode.ROOT_KEYGEN -> generateRootKeyPair() + DoormanParameters.Mode.CA_KEYGEN -> generateKeyPair() + DoormanParameters.Mode.DOORMAN -> startDoorman() + } + } } diff --git a/doorman/src/test/kotlin/com/r3/corda/doorman/DoormanParametersTest.kt b/doorman/src/test/kotlin/com/r3/corda/doorman/DoormanParametersTest.kt index 40fb2f3cfc..7767d010c0 100644 --- a/doorman/src/test/kotlin/com/r3/corda/doorman/DoormanParametersTest.kt +++ b/doorman/src/test/kotlin/com/r3/corda/doorman/DoormanParametersTest.kt @@ -19,5 +19,3 @@ class DoormanParametersTest { assertEquals(1000, params2.port) } } - - From 1e8c80855cca6a05467a42ab80a303029abe3bac Mon Sep 17 00:00:00 2001 From: Patrick Kuo Date: Mon, 13 Feb 2017 15:00:57 +0000 Subject: [PATCH 04/10] * Moved DoormanParameter to its own file * throws ShowHelpException instead of exitProcess in the helper method. --- .../com/r3/corda/doorman/DoormanParameters.kt | 49 ++++++++++++ .../main/kotlin/com/r3/corda/doorman/Main.kt | 80 +++++-------------- .../r3/corda/doorman/OptionParserUtilities.kt | 8 +- 3 files changed, 73 insertions(+), 64 deletions(-) create mode 100644 doorman/src/main/kotlin/com/r3/corda/doorman/DoormanParameters.kt 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 83ab2337cf..bd51d30a75 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/Main.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/Main.kt @@ -2,7 +2,6 @@ package com.r3.corda.doorman import com.google.common.net.HostAndPort import com.r3.corda.doorman.DoormanServer.Companion.logger -import com.r3.corda.doorman.OptionParserHelper.toConfigWithOptions import com.r3.corda.doorman.persistence.CertificationRequestStorage import com.r3.corda.doorman.persistence.DBCertificateRequestStorage import net.corda.core.createDirectories @@ -18,12 +17,8 @@ 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.div import net.corda.core.seconds import net.corda.core.utilities.loggerFor -import net.corda.node.services.config.ConfigHelper -import net.corda.node.services.config.getOrElse -import net.corda.node.services.config.getValue import net.corda.node.utilities.configureDatabase import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest import org.eclipse.jetty.server.Server @@ -36,10 +31,7 @@ import org.glassfish.jersey.servlet.ServletContainer import java.io.Closeable import java.lang.Thread.sleep import java.net.InetSocketAddress -import java.nio.file.Path -import java.nio.file.Paths import java.security.cert.Certificate -import java.util.* import kotlin.concurrent.thread import kotlin.system.exitProcess @@ -55,7 +47,7 @@ class DoormanServer(webServerAddr: HostAndPort, val caCertAndKey: CACertAndKey, } private val server: Server = Server(InetSocketAddress(webServerAddr.hostText, webServerAddr.port)).apply { - server.handler = HandlerCollection().apply { + handler = HandlerCollection().apply { addHandler(buildServletContextHandler()) } } @@ -93,43 +85,6 @@ class DoormanServer(webServerAddr: HostAndPort, val caCertAndKey: CACertAndKey, } } -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 - } -} - /** 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) { @@ -143,11 +98,11 @@ private fun readPassword(fmt: String): String { 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 : ") + 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 : ") + 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 @@ -164,17 +119,17 @@ private fun DoormanParameters.generateRootKeyPair() { println(loadKeyStore(rootStorePath, rootKeystorePassword).getCertificate(CORDA_ROOT_CA_PRIVATE_KEY).publicKey) } -private fun DoormanParameters.generateKeyPair() { +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 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 : ") + 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) @@ -197,8 +152,8 @@ private fun DoormanParameters.generateKeyPair() { 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 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) @@ -229,11 +184,16 @@ private fun DoormanParameters.startDoorman() { } fun main(args: Array) { - DoormanParameters(args).run { - when (mode) { - DoormanParameters.Mode.ROOT_KEYGEN -> generateRootKeyPair() - DoormanParameters.Mode.CA_KEYGEN -> generateKeyPair() - DoormanParameters.Mode.DOORMAN -> startDoorman() + try { + // 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() + } } + } catch (e: ShowHelpException) { + e.parser.printHelpOn(System.out) } } diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/OptionParserUtilities.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/OptionParserUtilities.kt index b88c32dcdc..54847f9407 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/OptionParserUtilities.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/OptionParserUtilities.kt @@ -4,7 +4,6 @@ import com.typesafe.config.Config import com.typesafe.config.ConfigFactory import joptsimple.ArgumentAcceptingOptionSpec import joptsimple.OptionParser -import kotlin.system.exitProcess /** * Convert commandline arguments to [Config] object will allow us to use kotlin delegate with [ConfigHelper]. @@ -17,8 +16,7 @@ object OptionParserHelper { val optionSet = parser.parse(*this) // Print help and exit on help option. if (optionSet.has(helpOption)) { - parser.printHelpOn(System.out) - exitProcess(0) + throw ShowHelpException(parser) } // Convert all command line options to Config. return ConfigFactory.parseMap(parser.recognizedOptions().mapValues { @@ -26,4 +24,6 @@ object OptionParserHelper { if (optionSpec is ArgumentAcceptingOptionSpec<*> && !optionSpec.requiresArgument() && optionSet.has(optionSpec)) true else optionSpec.value(optionSet) }.filterValues { it != null }) } -} \ No newline at end of file +} + +class ShowHelpException(val parser: OptionParser) : Exception() From 234863f88cd2c225d12495a2f1f9cb889440a217 Mon Sep 17 00:00:00 2001 From: Patrick Kuo Date: Wed, 15 Feb 2017 15:42:41 +0000 Subject: [PATCH 05/10] Doorman-Jira integration --- doorman/build.gradle | 7 ++ .../com/r3/corda/doorman/DoormanParameters.kt | 14 +++- .../main/kotlin/com/r3/corda/doorman/Main.kt | 40 +++++++----- .../CertificationRequestStorage.kt | 2 +- .../DBCertificateRequestStorage.kt | 4 +- .../JiraCertificateRequestStorage.kt | 65 +++++++++++++++++++ doorman/src/main/resources/reference.conf | 10 ++- .../r3/corda/doorman/DoormanServiceTest.kt | 4 +- .../DBCertificateRequestStorageTest.kt | 8 +-- 9 files changed, 126 insertions(+), 28 deletions(-) create mode 100644 doorman/src/main/kotlin/com/r3/corda/doorman/persistence/JiraCertificateRequestStorage.kt diff --git a/doorman/build.gradle b/doorman/build.gradle index 457d845d87..b78e7ce979 100644 --- a/doorman/build.gradle +++ b/doorman/build.gradle @@ -68,4 +68,11 @@ dependencies { testCompile 'junit:junit:4.12' testCompile "org.assertj:assertj-core:${assertj_version}" testCompile "com.nhaarman:mockito-kotlin:0.6.1" + + compile ('com.atlassian.jira:jira-rest-java-client-core:4.0.0'){ + // The jira client includes jersey-core 1.5 which breaks everything. + exclude module: 'jersey-core' + } + // Needed by jira rest client + compile "com.atlassian.fugue:fugue:2.6.1" } diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/DoormanParameters.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/DoormanParameters.kt index adee96471f..a0c9ca993d 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/DoormanParameters.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/DoormanParameters.kt @@ -1,6 +1,7 @@ package com.r3.corda.doorman import com.r3.corda.doorman.OptionParserHelper.toConfigWithOptions +import com.typesafe.config.Config import net.corda.core.div import net.corda.node.services.config.ConfigHelper import net.corda.node.services.config.getOrElse @@ -35,7 +36,8 @@ class DoormanParameters(args: Array) { val approveAll: Boolean by config.getOrElse { false } val host: String by config val port: Int by config - val dataSourceProperties: Properties by config + val dataSourceProperties: Properties by config + val jiraConfig = if (config.hasPath("jiraConfig")) JiraConfig(config.getConfig("jiraConfig")) else null private val keygen: Boolean by config.getOrElse { false } private val rootKeygen: Boolean by config.getOrElse { false } @@ -44,6 +46,12 @@ class DoormanParameters(args: Array) { enum class Mode { DOORMAN, CA_KEYGEN, ROOT_KEYGEN } + + class JiraConfig(config: Config) { + val address: String by config + val projectCode: String by config + val username: String by config + val password: String by config + val doneTransitionCode: Int by config + } } - - 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 bd51d30a75..7ed4adad34 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/Main.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/Main.kt @@ -1,9 +1,11 @@ package com.r3.corda.doorman +import com.atlassian.jira.rest.client.internal.async.AsynchronousJiraRestClientFactory 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 com.r3.corda.doorman.persistence.JiraCertificateRequestStorage import net.corda.core.createDirectories import net.corda.core.crypto.X509Utilities import net.corda.core.crypto.X509Utilities.CACertAndKey @@ -13,6 +15,7 @@ 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.createServerCert import net.corda.core.crypto.X509Utilities.loadCertificateAndKey import net.corda.core.crypto.X509Utilities.loadKeyStore import net.corda.core.crypto.X509Utilities.loadOrCreateKeyStore @@ -31,6 +34,7 @@ import org.glassfish.jersey.servlet.ServletContainer import java.io.Closeable import java.lang.Thread.sleep import java.net.InetSocketAddress +import java.net.URI import java.security.cert.Certificate import kotlin.concurrent.thread import kotlin.system.exitProcess @@ -159,22 +163,28 @@ private fun DoormanParameters.startDoorman() { 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 storage = if (approveAll) { + logger.warn("Doorman server is in 'Approve All' mode, this will approve all incoming certificate signing request.") + DBCertificateRequestStorage(database) + } else { + // Require JIRA config to be non-null. + val jiraClient = AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(URI(jiraConfig!!.address), jiraConfig.username, jiraConfig.password) + JiraCertificateRequestStorage(DBCertificateRequestStorage(database), jiraClient, jiraConfig.projectCode, jiraConfig.doneTransitionCode) + } + + // Daemon thread approving request periodically. + thread(name = "Request Approval Daemon") { + while (true) { + sleep(10.seconds.toMillis()) + val approvedRequests = (storage as? JiraCertificateRequestStorage)?.getRequestByStatus(JiraCertificateRequestStorage.APPROVED) ?: storage.getPendingRequestIds() + for (id in approvedRequests) { + storage.approveRequest(id) { + val request = JcaPKCS10CertificationRequest(request) + createServerCert(request.subject, request.publicKey, caCertAndKey, + if (ipAddress == hostName) listOf() else listOf(hostName), listOf(ipAddress)) } + logger.info("Approved request $id") } } } diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/CertificationRequestStorage.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/CertificationRequestStorage.kt index 0e9db59c0f..40aad83707 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/CertificationRequestStorage.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/CertificationRequestStorage.kt @@ -26,7 +26,7 @@ interface CertificationRequestStorage { /** * Approve the given request by generating and storing a new certificate using the provided generator. */ - fun approveRequest(requestId: String, certificateGenerator: (CertificationRequestData) -> Certificate) + fun approveRequest(requestId: String, generateCertificate: CertificationRequestData.() -> Certificate) /** * Reject the given request using the given reason. diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/DBCertificateRequestStorage.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/DBCertificateRequestStorage.kt index 1bd0809655..1c50e6469d 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/DBCertificateRequestStorage.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/DBCertificateRequestStorage.kt @@ -83,13 +83,13 @@ class DBCertificateRequestStorage(private val database: Database) : Certificatio } } - override fun approveRequest(requestId: String, certificateGenerator: (CertificationRequestData) -> Certificate) { + override fun approveRequest(requestId: String, generateCertificate: CertificationRequestData.() -> Certificate) { databaseTransaction(database) { val request = singleRequestWhere { DataTable.requestId eq requestId and DataTable.processTimestamp.isNull() } if (request != null) { withFinalizables { finalizables -> DataTable.update({ DataTable.requestId eq requestId }) { - it[certificate] = serializeToBlob(certificateGenerator(request), finalizables) + it[certificate] = serializeToBlob(request.generateCertificate(), finalizables) it[processTimestamp] = Instant.now() } } diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/JiraCertificateRequestStorage.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/JiraCertificateRequestStorage.kt new file mode 100644 index 0000000000..7eae257db1 --- /dev/null +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/JiraCertificateRequestStorage.kt @@ -0,0 +1,65 @@ +package com.r3.corda.doorman.persistence + +import com.atlassian.jira.rest.client.api.JiraRestClient +import com.atlassian.jira.rest.client.api.domain.input.IssueInputBuilder +import com.atlassian.jira.rest.client.api.domain.input.TransitionInput +import net.corda.core.crypto.X509Utilities +import net.corda.core.crypto.commonName +import org.bouncycastle.asn1.x500.style.BCStyle +import org.bouncycastle.openssl.jcajce.JcaPEMWriter +import org.bouncycastle.util.io.pem.PemObject +import java.io.ByteArrayInputStream +import java.io.StringWriter +import java.security.cert.Certificate + +class JiraCertificateRequestStorage(val delegate: CertificationRequestStorage, val jiraClient: JiraRestClient, val projectCode: String, val doneTransitionCode: Int) : CertificationRequestStorage by delegate { + companion object { + val APPROVED = "Approved" + val REJECTED = "Rejected" + } + + // The JIRA project must have a Request ID field and the Task issue type. + private val requestIdField = jiraClient.metadataClient.fields.claim().find { it.name == "Request ID" }!! + private val taskIssueType = jiraClient.metadataClient.issueTypes.claim().find { it.name == "Task" }!! + + override fun saveRequest(certificationData: CertificationRequestData): String { + val requestId = delegate.saveRequest(certificationData) + // Make sure request has been accepted. + val response = getResponse(requestId) + if (response !is CertificateResponse.Unauthorised) { + val request = StringWriter().use { + JcaPEMWriter(it).use { + it.writeObject(PemObject("CERTIFICATE REQUEST", certificationData.request.encoded)) + } + it.toString() + } + val commonName = certificationData.request.subject.commonName + val email = certificationData.request.subject.getRDNs(BCStyle.EmailAddress).firstOrNull()?.first?.value + val nearestCity = certificationData.request.subject.getRDNs(BCStyle.L).firstOrNull()?.first?.value + + val issue = IssueInputBuilder().setIssueTypeId(taskIssueType.id) + .setProjectKey("TD") + .setDescription("Legal Name: $commonName\nNearest City: $nearestCity\nEmail: $email\n\n{code}$request{code}") + .setSummary(commonName) + .setFieldValue(requestIdField.id, requestId) + + jiraClient.issueClient.createIssue(issue.build()).fail(Throwable::printStackTrace).claim() + } + return requestId + } + + override fun approveRequest(requestId: String, generateCertificate: CertificationRequestData.() -> Certificate) { + delegate.approveRequest(requestId, generateCertificate) + val certificate = (getResponse(requestId) as? CertificateResponse.Ready)?.certificate + val issue = jiraClient.searchClient.searchJql("'Request ID' ~ $requestId").claim().issues.firstOrNull() + issue?.let { + jiraClient.issueClient.transition(it, TransitionInput(doneTransitionCode)).fail(Throwable::printStackTrace) + jiraClient.issueClient.addAttachment(it.attachmentsUri, ByteArrayInputStream(certificate?.encoded), "${X509Utilities.CORDA_CLIENT_CA}.cer") + } + } + + fun getRequestByStatus(status: String): List { + val issues = jiraClient.searchClient.searchJql("project = $projectCode AND status = $status").claim().issues + return issues.map { it.getField(requestIdField.id)?.value?.toString() }.filterNotNull() + } +} diff --git a/doorman/src/main/resources/reference.conf b/doorman/src/main/resources/reference.conf index 446362dba0..433603fc8d 100644 --- a/doorman/src/main/resources/reference.conf +++ b/doorman/src/main/resources/reference.conf @@ -11,4 +11,12 @@ dataSourceProperties { "dataSource.user" = sa "dataSource.password" = "" } -h2port = 0 \ No newline at end of file +h2port = 0 + +jiraConfig{ + address = "https://doorman-jira-host/" + projectCode = "TD" + username = "username" + password = "password" + doneTransitionCode = 41 +} 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 6ad0fb07d4..314ba29945 100644 --- a/doorman/src/test/kotlin/com/r3/corda/doorman/DoormanServiceTest.kt +++ b/doorman/src/test/kotlin/com/r3/corda/doorman/DoormanServiceTest.kt @@ -86,9 +86,9 @@ class DoormanServiceTest { assertThat(pollForResponse(id)).isEqualTo(PollResponse.NotReady) storage.approveRequest(id) { - JcaPKCS10CertificationRequest(it.request).run { + JcaPKCS10CertificationRequest(request).run { X509Utilities.createServerCert(subject, publicKey, intermediateCA, - if (it.ipAddress == it.hostName) listOf() else listOf(it.hostName), listOf(it.ipAddress)) + if (ipAddress == hostName) listOf() else listOf(hostName), listOf(ipAddress)) } } diff --git a/doorman/src/test/kotlin/com/r3/corda/doorman/internal/persistence/DBCertificateRequestStorageTest.kt b/doorman/src/test/kotlin/com/r3/corda/doorman/internal/persistence/DBCertificateRequestStorageTest.kt index 6a84306b33..6b0593441a 100644 --- a/doorman/src/test/kotlin/com/r3/corda/doorman/internal/persistence/DBCertificateRequestStorageTest.kt +++ b/doorman/src/test/kotlin/com/r3/corda/doorman/internal/persistence/DBCertificateRequestStorageTest.kt @@ -133,14 +133,14 @@ class DBCertificateRequestStorageTest { private fun approveRequest(requestId: String) { storage.approveRequest(requestId) { - JcaPKCS10CertificationRequest(it.request).run { + JcaPKCS10CertificationRequest(request).run { X509Utilities.createServerCert( subject, publicKey, intermediateCA, - if (it.ipAddress == it.hostName) listOf() else listOf(it.hostName), - listOf(it.ipAddress)) + if (ipAddress == hostName) listOf() else listOf(hostName), + listOf(ipAddress)) } } } -} \ No newline at end of file +} From d76db6d047163e9ed132627295c34db729b61a04 Mon Sep 17 00:00:00 2001 From: Patrick Kuo Date: Thu, 16 Feb 2017 11:30:03 +0000 Subject: [PATCH 06/10] Addressed PR issues --- .../com/r3/corda/doorman/DoormanParameters.kt | 2 - .../main/kotlin/com/r3/corda/doorman/Main.kt | 14 +++--- .../CertificationRequestStorage.kt | 6 ++- .../DBCertificateRequestStorage.kt | 2 + .../JiraCertificateRequestStorage.kt | 47 +++++++++++-------- doorman/src/main/resources/reference.conf | 1 - .../r3/corda/doorman/DoormanParametersTest.kt | 3 +- 7 files changed, 44 insertions(+), 31 deletions(-) diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/DoormanParameters.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/DoormanParameters.kt index a0c9ca993d..805dc10d06 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/DoormanParameters.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/DoormanParameters.kt @@ -15,7 +15,6 @@ class DoormanParameters(args: Array) { 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") @@ -33,7 +32,6 @@ class DoormanParameters(args: Array) { 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 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 7ed4adad34..5b5a837490 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/Main.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/Main.kt @@ -164,12 +164,14 @@ private fun DoormanParameters.startDoorman() { // Create DB connection. val (datasource, database) = configureDatabase(dataSourceProperties) - val storage = if (approveAll) { + val storage = if (jiraConfig == null) { logger.warn("Doorman server is in 'Approve All' mode, this will approve all incoming certificate signing request.") - DBCertificateRequestStorage(database) + // Approve all pending request. + object : CertificationRequestStorage by DBCertificateRequestStorage(database) { + override fun getApprovedRequestIds() = getPendingRequestIds() + } } else { - // Require JIRA config to be non-null. - val jiraClient = AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(URI(jiraConfig!!.address), jiraConfig.username, jiraConfig.password) + val jiraClient = AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(URI(jiraConfig.address), jiraConfig.username, jiraConfig.password) JiraCertificateRequestStorage(DBCertificateRequestStorage(database), jiraClient, jiraConfig.projectCode, jiraConfig.doneTransitionCode) } @@ -177,8 +179,8 @@ private fun DoormanParameters.startDoorman() { thread(name = "Request Approval Daemon") { while (true) { sleep(10.seconds.toMillis()) - val approvedRequests = (storage as? JiraCertificateRequestStorage)?.getRequestByStatus(JiraCertificateRequestStorage.APPROVED) ?: storage.getPendingRequestIds() - for (id in approvedRequests) { + // TODO: Handle rejected request? + for (id in storage.getApprovedRequestIds()) { storage.approveRequest(id) { val request = JcaPKCS10CertificationRequest(request) createServerCert(request.subject, request.publicKey, caCertAndKey, diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/CertificationRequestStorage.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/CertificationRequestStorage.kt index 40aad83707..3f63756f20 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/CertificationRequestStorage.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/CertificationRequestStorage.kt @@ -35,9 +35,13 @@ interface CertificationRequestStorage { /** * Retrieve list of request IDs waiting for approval. - * TODO : This is used for the background thread to approve request automatically without KYC checks, should be removed after testnet. */ fun getPendingRequestIds(): List + + /** + * Retrieve list of approved request IDs. + */ + fun getApprovedRequestIds(): List } data class CertificationRequestData(val hostName: String, val ipAddress: String, val request: PKCS10CertificationRequest) diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/DBCertificateRequestStorage.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/DBCertificateRequestStorage.kt index 1c50e6469d..62282b1254 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/DBCertificateRequestStorage.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/DBCertificateRequestStorage.kt @@ -121,6 +121,8 @@ class DBCertificateRequestStorage(private val database: Database) : Certificatio } } + override fun getApprovedRequestIds(): List = emptyList() + private fun singleRequestWhere(where: SqlExpressionBuilder.() -> Op): CertificationRequestData? { return DataTable .select(where) diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/JiraCertificateRequestStorage.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/JiraCertificateRequestStorage.kt index 7eae257db1..d79c7c7f92 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/JiraCertificateRequestStorage.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/JiraCertificateRequestStorage.kt @@ -1,64 +1,73 @@ package com.r3.corda.doorman.persistence import com.atlassian.jira.rest.client.api.JiraRestClient +import com.atlassian.jira.rest.client.api.domain.Field +import com.atlassian.jira.rest.client.api.domain.IssueType import com.atlassian.jira.rest.client.api.domain.input.IssueInputBuilder import com.atlassian.jira.rest.client.api.domain.input.TransitionInput import net.corda.core.crypto.X509Utilities import net.corda.core.crypto.commonName +import net.corda.core.utilities.loggerFor import org.bouncycastle.asn1.x500.style.BCStyle import org.bouncycastle.openssl.jcajce.JcaPEMWriter import org.bouncycastle.util.io.pem.PemObject -import java.io.ByteArrayInputStream import java.io.StringWriter import java.security.cert.Certificate -class JiraCertificateRequestStorage(val delegate: CertificationRequestStorage, val jiraClient: JiraRestClient, val projectCode: String, val doneTransitionCode: Int) : CertificationRequestStorage by delegate { - companion object { - val APPROVED = "Approved" - val REJECTED = "Rejected" +class JiraCertificateRequestStorage(val delegate: CertificationRequestStorage, + val jiraClient: JiraRestClient, + val projectCode: String, + val doneTransitionCode: Int) : CertificationRequestStorage by delegate { + private enum class Status { + Approved, Rejected } + private val logger = loggerFor() + // The JIRA project must have a Request ID field and the Task issue type. - private val requestIdField = jiraClient.metadataClient.fields.claim().find { it.name == "Request ID" }!! - private val taskIssueType = jiraClient.metadataClient.issueTypes.claim().find { it.name == "Task" }!! + private val requestIdField: Field = jiraClient.metadataClient.fields.claim().find { it.name == "Request ID" }!! + private val taskIssueType: IssueType = jiraClient.metadataClient.issueTypes.claim().find { it.name == "Task" }!! override fun saveRequest(certificationData: CertificationRequestData): String { val requestId = delegate.saveRequest(certificationData) // Make sure request has been accepted. val response = getResponse(requestId) if (response !is CertificateResponse.Unauthorised) { - val request = StringWriter().use { - JcaPEMWriter(it).use { - it.writeObject(PemObject("CERTIFICATE REQUEST", certificationData.request.encoded)) - } - it.toString() + val request = StringWriter() + JcaPEMWriter(request).use { + it.writeObject(PemObject("CERTIFICATE REQUEST", certificationData.request.encoded)) } val commonName = certificationData.request.subject.commonName val email = certificationData.request.subject.getRDNs(BCStyle.EmailAddress).firstOrNull()?.first?.value val nearestCity = certificationData.request.subject.getRDNs(BCStyle.L).firstOrNull()?.first?.value val issue = IssueInputBuilder().setIssueTypeId(taskIssueType.id) - .setProjectKey("TD") + .setProjectKey(projectCode) .setDescription("Legal Name: $commonName\nNearest City: $nearestCity\nEmail: $email\n\n{code}$request{code}") .setSummary(commonName) .setFieldValue(requestIdField.id, requestId) - - jiraClient.issueClient.createIssue(issue.build()).fail(Throwable::printStackTrace).claim() + // This will block until the jira is created. + jiraClient.issueClient.createIssue(issue.build()).fail { logger.error("Exception when creating JIRA issue.", it) }.claim() } return requestId } override fun approveRequest(requestId: String, generateCertificate: CertificationRequestData.() -> Certificate) { delegate.approveRequest(requestId, generateCertificate) + // Certificate should be created, retrieving it to attach to the jira task. val certificate = (getResponse(requestId) as? CertificateResponse.Ready)?.certificate + // Jira only support ~ (contains) search for custom textfield. val issue = jiraClient.searchClient.searchJql("'Request ID' ~ $requestId").claim().issues.firstOrNull() - issue?.let { - jiraClient.issueClient.transition(it, TransitionInput(doneTransitionCode)).fail(Throwable::printStackTrace) - jiraClient.issueClient.addAttachment(it.attachmentsUri, ByteArrayInputStream(certificate?.encoded), "${X509Utilities.CORDA_CLIENT_CA}.cer") + if (issue != null) { + jiraClient.issueClient.transition(issue, TransitionInput(doneTransitionCode)).fail { logger.error("Exception when transiting JIRA status.", it) }.claim() + jiraClient.issueClient.addAttachment(issue.attachmentsUri, certificate?.encoded?.inputStream(), "${X509Utilities.CORDA_CLIENT_CA}.cer") + .fail { logger.error("Exception when uploading attachment to JIRA.", it) }.claim() } } - fun getRequestByStatus(status: String): List { + override fun getApprovedRequestIds(): List = getRequestByStatus(Status.Approved) + + private fun getRequestByStatus(status: Status): List { val issues = jiraClient.searchClient.searchJql("project = $projectCode AND status = $status").claim().issues return issues.map { it.getField(requestIdField.id)?.value?.toString() }.filterNotNull() } diff --git a/doorman/src/main/resources/reference.conf b/doorman/src/main/resources/reference.conf index 433603fc8d..dcf1ccff1c 100644 --- a/doorman/src/main/resources/reference.conf +++ b/doorman/src/main/resources/reference.conf @@ -3,7 +3,6 @@ port = 0 keystorePath = ${basedir}"/certificates/caKeystore.jks" keystorePassword = "password" caPrivateKeyPassword = "password" -approveAll = true dataSourceProperties { dataSourceClassName = org.h2.jdbcx.JdbcDataSource diff --git a/doorman/src/test/kotlin/com/r3/corda/doorman/DoormanParametersTest.kt b/doorman/src/test/kotlin/com/r3/corda/doorman/DoormanParametersTest.kt index 7767d010c0..2f53e80e02 100644 --- a/doorman/src/test/kotlin/com/r3/corda/doorman/DoormanParametersTest.kt +++ b/doorman/src/test/kotlin/com/r3/corda/doorman/DoormanParametersTest.kt @@ -7,11 +7,10 @@ import kotlin.test.assertTrue class DoormanParametersTest { @Test fun `parse arg correctly`() { - val params = DoormanParameters(arrayOf("--keygen", "--keystorePath", "./testDummyPath.jks", "--approveAll")) + val params = DoormanParameters(arrayOf("--keygen", "--keystorePath", "./testDummyPath.jks")) 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) From e2f93f64a96576df3c93b313e42e458ed8a6d40f Mon Sep 17 00:00:00 2001 From: Patrick Kuo Date: Thu, 16 Feb 2017 14:45:36 +0000 Subject: [PATCH 07/10] Addressed PR issues --- doorman/src/main/kotlin/com/r3/corda/doorman/Main.kt | 1 + .../persistence/JiraCertificateRequestStorage.kt | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) 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 5b5a837490..a33f924131 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/Main.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/Main.kt @@ -168,6 +168,7 @@ private fun DoormanParameters.startDoorman() { logger.warn("Doorman server is in 'Approve All' mode, this will approve all incoming certificate signing request.") // Approve all pending request. object : CertificationRequestStorage by DBCertificateRequestStorage(database) { + // The doorman is in approve all mode, returns all pending request id as approved request id. override fun getApprovedRequestIds() = getPendingRequestIds() } } else { diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/JiraCertificateRequestStorage.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/JiraCertificateRequestStorage.kt index d79c7c7f92..4d3ee9c511 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/JiraCertificateRequestStorage.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/JiraCertificateRequestStorage.kt @@ -22,7 +22,9 @@ class JiraCertificateRequestStorage(val delegate: CertificationRequestStorage, Approved, Rejected } - private val logger = loggerFor() + companion object{ + private val logger = loggerFor() + } // The JIRA project must have a Request ID field and the Task issue type. private val requestIdField: Field = jiraClient.metadataClient.fields.claim().find { it.name == "Request ID" }!! @@ -34,9 +36,7 @@ class JiraCertificateRequestStorage(val delegate: CertificationRequestStorage, val response = getResponse(requestId) if (response !is CertificateResponse.Unauthorised) { val request = StringWriter() - JcaPEMWriter(request).use { - it.writeObject(PemObject("CERTIFICATE REQUEST", certificationData.request.encoded)) - } + JcaPEMWriter(request).writeObject(PemObject("CERTIFICATE REQUEST", certificationData.request.encoded)) val commonName = certificationData.request.subject.commonName val email = certificationData.request.subject.getRDNs(BCStyle.EmailAddress).firstOrNull()?.first?.value val nearestCity = certificationData.request.subject.getRDNs(BCStyle.L).firstOrNull()?.first?.value @@ -46,7 +46,7 @@ class JiraCertificateRequestStorage(val delegate: CertificationRequestStorage, .setDescription("Legal Name: $commonName\nNearest City: $nearestCity\nEmail: $email\n\n{code}$request{code}") .setSummary(commonName) .setFieldValue(requestIdField.id, requestId) - // This will block until the jira is created. + // This will block until the issue is created. jiraClient.issueClient.createIssue(issue.build()).fail { logger.error("Exception when creating JIRA issue.", it) }.claim() } return requestId From 5064cbb5c005a6526ec9413fc9c865fd731e1051 Mon Sep 17 00:00:00 2001 From: Patrick Kuo Date: Thu, 16 Feb 2017 14:47:20 +0000 Subject: [PATCH 08/10] Addressed PR issues --- doorman/src/main/kotlin/com/r3/corda/doorman/Main.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 a33f924131..93e0b6ec89 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/Main.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/Main.kt @@ -164,16 +164,18 @@ private fun DoormanParameters.startDoorman() { // Create DB connection. val (datasource, database) = configureDatabase(dataSourceProperties) + val requestStorage = DBCertificateRequestStorage(database) + val storage = if (jiraConfig == null) { logger.warn("Doorman server is in 'Approve All' mode, this will approve all incoming certificate signing request.") // Approve all pending request. - object : CertificationRequestStorage by DBCertificateRequestStorage(database) { + object : CertificationRequestStorage by requestStorage { // The doorman is in approve all mode, returns all pending request id as approved request id. override fun getApprovedRequestIds() = getPendingRequestIds() } } else { val jiraClient = AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(URI(jiraConfig.address), jiraConfig.username, jiraConfig.password) - JiraCertificateRequestStorage(DBCertificateRequestStorage(database), jiraClient, jiraConfig.projectCode, jiraConfig.doneTransitionCode) + JiraCertificateRequestStorage(requestStorage, jiraClient, jiraConfig.projectCode, jiraConfig.doneTransitionCode) } // Daemon thread approving request periodically. From f999da3a4d1d314840664b3d4a671c4b72a8595c Mon Sep 17 00:00:00 2001 From: Patrick Kuo Date: Thu, 16 Feb 2017 15:11:25 +0000 Subject: [PATCH 09/10] Addressed PR issues --- .../doorman/persistence/JiraCertificateRequestStorage.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/JiraCertificateRequestStorage.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/JiraCertificateRequestStorage.kt index 4d3ee9c511..b7366f9b24 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/JiraCertificateRequestStorage.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/JiraCertificateRequestStorage.kt @@ -36,7 +36,9 @@ class JiraCertificateRequestStorage(val delegate: CertificationRequestStorage, val response = getResponse(requestId) if (response !is CertificateResponse.Unauthorised) { val request = StringWriter() - JcaPEMWriter(request).writeObject(PemObject("CERTIFICATE REQUEST", certificationData.request.encoded)) + JcaPEMWriter(request).use{ + it.writeObject(PemObject("CERTIFICATE REQUEST", certificationData.request.encoded)) + } val commonName = certificationData.request.subject.commonName val email = certificationData.request.subject.getRDNs(BCStyle.EmailAddress).firstOrNull()?.first?.value val nearestCity = certificationData.request.subject.getRDNs(BCStyle.L).firstOrNull()?.first?.value From 1611066c443d54cb1bf9b85fa9f65220f68df73f Mon Sep 17 00:00:00 2001 From: Patrick Kuo Date: Thu, 16 Feb 2017 15:13:05 +0000 Subject: [PATCH 10/10] Addressed PR issues --- .../doorman/persistence/JiraCertificateRequestStorage.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/JiraCertificateRequestStorage.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/JiraCertificateRequestStorage.kt index b7366f9b24..bca5678d43 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/JiraCertificateRequestStorage.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/persistence/JiraCertificateRequestStorage.kt @@ -22,7 +22,7 @@ class JiraCertificateRequestStorage(val delegate: CertificationRequestStorage, Approved, Rejected } - companion object{ + companion object { private val logger = loggerFor() } @@ -36,7 +36,7 @@ class JiraCertificateRequestStorage(val delegate: CertificationRequestStorage, val response = getResponse(requestId) if (response !is CertificateResponse.Unauthorised) { val request = StringWriter() - JcaPEMWriter(request).use{ + JcaPEMWriter(request).use { it.writeObject(PemObject("CERTIFICATE REQUEST", certificationData.request.encoded)) } val commonName = certificationData.request.subject.commonName