Merged in pat-add-keygen-option-to-doorman (pull request #18)

Add keygen option to doorman

Approved-by: Shams Asari
This commit is contained in:
Patrick Kuo 2017-02-14 09:48:22 +00:00
commit 7cd8d952a9
6 changed files with 242 additions and 77 deletions

View File

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

View File

@ -1,14 +1,24 @@
package com.r3.corda.doorman package com.r3.corda.doorman
import com.google.common.net.HostAndPort 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.DBCertificateRequestStorage
import joptsimple.ArgumentAcceptingOptionSpec import net.corda.core.createDirectories
import joptsimple.OptionParser
import net.corda.core.crypto.X509Utilities 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.core.utilities.loggerFor
import net.corda.node.services.config.ConfigHelper
import net.corda.node.services.config.getProperties
import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.configureDatabase
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
import org.eclipse.jetty.server.Server 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.server.ResourceConfig
import org.glassfish.jersey.servlet.ServletContainer import org.glassfish.jersey.servlet.ServletContainer
import java.io.Closeable import java.io.Closeable
import java.lang.Thread.sleep
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.nio.file.Paths import java.security.cert.Certificate
import kotlin.concurrent.thread import kotlin.concurrent.thread
import kotlin.system.exitProcess 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 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] * 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 { companion object {
val log = loggerFor<DoormanServer>() val logger = loggerFor<DoormanServer>()
}
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 val hostAndPort: HostAndPort get() = server.connectors
.map { it as? ServerConnector } .map { it as? ServerConnector }
.filterNotNull() .filterNotNull()
@ -42,20 +59,15 @@ class DoormanServer(val webServerAddr: HostAndPort, val doormanWebService: Doorm
.first() .first()
override fun close() { override fun close() {
log.info("Shutting down CertificateSigningService...") logger.info("Shutting down Doorman Web Services...")
server.stop() server.stop()
server.join() server.join()
} }
private fun initWebServer(): Server { fun start() {
return Server(InetSocketAddress(webServerAddr.hostText, webServerAddr.port)).apply { logger.info("Starting Doorman Web Services...")
log.info("Starting CertificateSigningService...") server.start()
handler = HandlerCollection().apply { logger.info("Doorman Web Services started on $hostAndPort")
addHandler(buildServletContextHandler())
}
start()
log.info("CertificateSigningService started on $hostAndPort")
}
} }
private fun buildServletContextHandler(): ServletContextHandler { private fun buildServletContextHandler(): ServletContextHandler {
@ -63,7 +75,7 @@ class DoormanServer(val webServerAddr: HostAndPort, val doormanWebService: Doorm
contextPath = "/" contextPath = "/"
val resourceConfig = ResourceConfig().apply { val resourceConfig = ResourceConfig().apply {
// Add your API provider classes (annotated for JAX-RS) here // Add your API provider classes (annotated for JAX-RS) here
register(doormanWebService) register(DoormanWebService(caCertAndKey, rootCACert, storage))
} }
val jerseyServlet = ServletHolder(ServletContainer(resourceConfig)).apply { val jerseyServlet = ServletHolder(ServletContainer(resourceConfig)).apply {
initOrder = 0 // Initialise at server start initOrder = 0 // Initialise at server start
@ -73,62 +85,115 @@ class DoormanServer(val webServerAddr: HostAndPort, val doormanWebService: Doorm
} }
} }
object ParamsSpec { /** Read password from console, do a readLine instead if console is null (e.g. when debugging in IDE). */
val parser = OptionParser() private fun readPassword(fmt: String): String {
val basedir: ArgumentAcceptingOptionSpec<String>? = parser.accepts("basedir", "Overriding configuration file path.") return if (System.console() != null) {
.withRequiredArg() 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<String>) { fun main(args: Array<String>) {
val log = DoormanServer.log
log.info("Starting certificate signing server.")
try { try {
ParamsSpec.parser.parse(*args) // TODO : Remove config overrides and solely use config file after testnet is finalized.
} catch (ex: Exception) { DoormanParameters(args).run {
log.error("Unable to parse args", ex) when (mode) {
ParamsSpec.parser.printHelpOn(System.out) DoormanParameters.Mode.ROOT_KEYGEN -> generateRootKeyPair()
exitProcess(1) DoormanParameters.Mode.CA_KEYGEN -> generateCAKeyPair()
}.run { DoormanParameters.Mode.DOORMAN -> startDoorman()
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." }
} }
} 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)
} }
} }

View File

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

View File

@ -1,8 +1,8 @@
host = localhost host = localhost
port = 0 port = 0
keystorePath = ${basedir}"/certificates/keystore.jks" keystorePath = ${basedir}"/certificates/caKeystore.jks"
keyStorePassword = "password" keystorePassword = "password"
caKeyPassword = "password" caPrivateKeyPassword = "password"
approveAll = true approveAll = true
dataSourceProperties { dataSourceProperties {

View File

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

View File

@ -31,7 +31,8 @@ class DoormanServiceTest {
private lateinit var doormanServer: DoormanServer private lateinit var doormanServer: DoormanServer
private fun startSigningServer(storage: CertificationRequestStorage) { 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 @After