Make Network registration process more verbose (#251)

* Make the network registration process more verbose
* removed gradle task for building standalone jar for the certificate signing request utility
* Added a flag "--initial-registration" to the corda jar to start the registration
This commit is contained in:
Patrick Kuo
2017-02-20 13:22:37 +00:00
committed by GitHub
parent 99721bf8f1
commit 9a0a9567f3
13 changed files with 192 additions and 189 deletions

View File

@ -29,6 +29,7 @@ class ArgsParser {
.defaultsTo(Level.INFO)
private val logToConsoleArg = optionParser.accepts("log-to-console", "If set, prints logging to the console as well as to a file.")
private val isWebserverArg = optionParser.accepts("webserver")
private val isRegistrationArg = optionParser.accepts("initial-registration", "Start initial node registration with Corda network to obtain certificate from the permissioning server.")
private val helpArg = optionParser.accepts("help").forHelp()
fun parse(vararg args: String): CmdLineOptions {
@ -42,7 +43,8 @@ class ArgsParser {
val loggingLevel = optionSet.valueOf(loggerLevel)
val logToConsole = optionSet.has(logToConsoleArg)
val isWebserver = optionSet.has(isWebserverArg)
return CmdLineOptions(baseDirectory, configFile, help, loggingLevel, logToConsole, isWebserver)
val isRegistration = optionSet.has(isRegistrationArg)
return CmdLineOptions(baseDirectory, configFile, help, loggingLevel, logToConsole, isWebserver, isRegistration)
}
fun printHelp(sink: PrintStream) = optionParser.printHelpOn(sink)
@ -53,7 +55,8 @@ data class CmdLineOptions(val baseDirectory: Path,
val help: Boolean,
val loggingLevel: Level,
val logToConsole: Boolean,
val isWebserver: Boolean) {
val isWebserver: Boolean,
val isRegistration: Boolean) {
fun loadConfig(allowMissingConfig: Boolean = false, configOverrides: Map<String, Any?> = emptyMap()): Config {
return ConfigHelper.loadConfig(baseDirectory, configFile, allowMissingConfig, configOverrides)
}

View File

@ -8,6 +8,8 @@ import net.corda.core.utilities.Emoji
import net.corda.node.internal.Node
import net.corda.node.services.config.FullNodeConfiguration
import net.corda.node.utilities.ANSIProgressObserver
import net.corda.node.utilities.registration.HTTPNetworkRegistrationService
import net.corda.node.utilities.registration.NetworkRegistrationHelper
import net.corda.node.webserver.WebServer
import org.fusesource.jansi.Ansi
import org.fusesource.jansi.AnsiConsole
@ -72,6 +74,17 @@ fun main(args: Array<String>) {
exitProcess(2)
}
if (cmdlineOptions.isRegistration) {
println()
println("******************************************************************")
println("* *")
println("* Registering as a new participant with Corda network *")
println("* *")
println("******************************************************************")
NetworkRegistrationHelper(conf, HTTPNetworkRegistrationService(conf.certificateSigningService)).buildKeystore()
exitProcess(0)
}
log.info("Main class: ${FullNodeConfiguration::class.java.protectionDomain.codeSource.location.toURI().path}")
val info = ManagementFactory.getRuntimeMXBean()
log.info("CommandLine Args: ${info.inputArguments.joinToString(" ")}")
@ -81,7 +94,7 @@ fun main(args: Array<String>) {
log.info("VM ${info.vmName} ${info.vmVendor} ${info.vmVersion}")
log.info("Machine: ${InetAddress.getLocalHost().hostName}")
log.info("Working Directory: ${cmdlineOptions.baseDirectory}")
if(cmdlineOptions.isWebserver) {
if (cmdlineOptions.isWebserver) {
log.info("Starting as webserver on ${conf.webAddress}")
} else {
log.info("Starting as node on ${conf.artemisAddress}")
@ -91,7 +104,7 @@ fun main(args: Array<String>) {
cmdlineOptions.baseDirectory.createDirectories()
// TODO: Webserver should be split and start from inside a WAR container
if (!cmdlineOptions.isWebserver) {
if (!cmdlineOptions.isWebserver) {
val node = conf.createNode()
node.start()
printPluginsAndServices(node)

View File

@ -191,7 +191,10 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
log.warn("Corda node is running in dev mode.")
configuration.configureWithDevSSLCertificate()
}
require(hasSSLCertificates()) { "SSL certificates not found." }
require(hasSSLCertificates()) { "Identity certificate not found. " +
"Please either copy your existing identity key and certificate from another node, " +
"or if you don't have one yet, fill out the config file and run corda.jar --initial-registration. " +
"Read more at: https://docs.corda.net/permissioning.html" }
log.info("Node starting up ...")

View File

@ -10,6 +10,7 @@ import net.corda.node.serialization.NodeClock
import net.corda.node.services.User
import net.corda.node.services.network.NetworkMapService
import net.corda.node.utilities.TestClock
import java.net.URL
import java.nio.file.Path
import java.util.*
@ -32,6 +33,7 @@ interface NodeConfiguration : SSLConfiguration {
val dataSourceProperties: Properties get() = Properties()
val rpcUsers: List<User> get() = emptyList()
val devMode: Boolean
val certificateSigningService: URL
}
/**
@ -46,6 +48,7 @@ class FullNodeConfiguration(override val baseDirectory: Path, val config: Config
override val trustStorePassword: String by config
override val dataSourceProperties: Properties by config
override val devMode: Boolean by config.getOrElse { false }
override val certificateSigningService: URL by config
override val networkMapService: NetworkMapInfo? = config.getOptionalConfig("networkMapService")?.run {
NetworkMapInfo(
HostAndPort.fromString(getString("address")),

View File

@ -1,131 +0,0 @@
package net.corda.node.utilities.certsigning
import net.corda.core.*
import net.corda.core.crypto.X509Utilities
import net.corda.core.crypto.X509Utilities.CORDA_CLIENT_CA
import net.corda.core.crypto.X509Utilities.CORDA_CLIENT_CA_PRIVATE_KEY
import net.corda.core.crypto.X509Utilities.CORDA_ROOT_CA
import net.corda.core.crypto.X509Utilities.addOrReplaceCertificate
import net.corda.core.crypto.X509Utilities.addOrReplaceKey
import net.corda.core.utilities.loggerFor
import net.corda.node.ArgsParser
import net.corda.node.services.config.FullNodeConfiguration
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.config.getValue
import net.corda.node.utilities.certsigning.CertificateSigner.Companion.log
import java.net.URL
import java.security.KeyPair
import java.security.cert.Certificate
import kotlin.system.exitProcess
/**
* This check the [certificatePath] for certificates required to connect to the Corda network.
* If the certificates are not found, a [PKCS10CertificationRequest] will be submitted to Corda network permissioning server using [CertificateSigningService].
* This process will enter a slow polling loop until the request has been approved, and then
* the certificate chain will be downloaded and stored in [KeyStore] reside in [certificatePath].
*/
class CertificateSigner(val config: NodeConfiguration, val certService: CertificateSigningService) {
companion object {
val pollInterval = 1.minutes
val log = loggerFor<CertificateSigner>()
}
fun buildKeyStore() {
config.certificatesDirectory.createDirectories()
val caKeyStore = X509Utilities.loadOrCreateKeyStore(config.keyStoreFile, config.keyStorePassword)
if (!caKeyStore.containsAlias(CORDA_CLIENT_CA)) {
// No certificate found in key store, create certificate signing request and post request to signing server.
log.info("No certificate found in key store, creating certificate signing request...")
// Create or load key pair from the key store.
val keyPair = X509Utilities.loadOrCreateKeyPairFromKeyStore(config.keyStoreFile, config.keyStorePassword,
config.keyStorePassword, CORDA_CLIENT_CA_PRIVATE_KEY) {
X509Utilities.createSelfSignedCACert(config.myLegalName)
}
log.info("Submitting certificate signing request to Corda certificate signing server.")
val requestId = submitCertificateSigningRequest(keyPair)
log.info("Successfully submitted request to Corda certificate signing server, request ID : $requestId")
log.info("Start polling server for certificate signing approval.")
val certificates = pollServerForCertificates(requestId)
log.info("Certificate signing request approved, installing new certificates.")
// Save private key and certificate chain to the key store.
caKeyStore.addOrReplaceKey(CORDA_CLIENT_CA_PRIVATE_KEY, keyPair.private,
config.keyStorePassword.toCharArray(), certificates)
// Assumes certificate chain always starts with client certificate and end with root certificate.
caKeyStore.addOrReplaceCertificate(CORDA_CLIENT_CA, certificates.first())
X509Utilities.saveKeyStore(caKeyStore, config.keyStoreFile, config.keyStorePassword)
// Save certificates to trust store.
val trustStore = X509Utilities.loadOrCreateKeyStore(config.trustStoreFile, config.trustStorePassword)
// Assumes certificate chain always starts with client certificate and end with root certificate.
trustStore.addOrReplaceCertificate(CORDA_ROOT_CA, certificates.last())
X509Utilities.saveKeyStore(trustStore, config.trustStoreFile, config.trustStorePassword)
} else {
log.trace("Certificate already exists, exiting certificate signer...")
}
}
/**
* Poll Certificate Signing Server for approved certificate,
* enter a slow polling loop if server return null.
* @param requestId Certificate signing request ID.
* @return Map of certificate chain.
*/
private fun pollServerForCertificates(requestId: String): Array<Certificate> {
// Poll server to download the signed certificate once request has been approved.
var certificates = certService.retrieveCertificates(requestId)
while (certificates == null) {
Thread.sleep(pollInterval.toMillis())
certificates = certService.retrieveCertificates(requestId)
}
return certificates
}
/**
* Submit Certificate Signing Request to Certificate signing service if request ID not found in file system
* New request ID will be stored in requestId.txt
* @param keyPair Public Private key pair generated for SSL certification.
* @return Request ID return from the server.
*/
private fun submitCertificateSigningRequest(keyPair: KeyPair): String {
val requestIdStore = config.certificatesDirectory / "certificate-request-id.txt"
// Retrieve request id from file if exists, else post a request to server.
return if (!requestIdStore.exists()) {
val request = X509Utilities.createCertificateSigningRequest(config.myLegalName, config.nearestCity, config.emailAddress, keyPair)
// Post request to signing server via http.
val requestId = certService.submitRequest(request)
// Persists request ID to file in case of node shutdown.
requestIdStore.writeLines(listOf(requestId))
requestId
} else {
requestIdStore.readLines { it.findFirst().get() }
}
}
}
fun main(args: Array<String>) {
val argsParser = ArgsParser()
val cmdlineOptions = try {
argsParser.parse(*args)
} catch (ex: Exception) {
log.error("Unable to parse args", ex)
argsParser.printHelp(System.out)
exitProcess(1)
}
val config = cmdlineOptions.loadConfig(allowMissingConfig = true)
val configuration = object : NodeConfiguration by FullNodeConfiguration(cmdlineOptions.baseDirectory, config) {
val certificateSigningService: URL by config
}
// TODO: Use HTTPS instead
CertificateSigner(configuration, HTTPCertificateSigningService(configuration.certificateSigningService)).buildKeyStore()
}

View File

@ -1,4 +1,4 @@
package net.corda.node.utilities.certsigning
package net.corda.node.utilities.registration
import net.corda.core.crypto.CertificateStream
import org.apache.commons.io.IOUtils
@ -11,12 +11,13 @@ import java.security.cert.Certificate
import java.util.*
import java.util.zip.ZipInputStream
class HTTPCertificateSigningService(val server: URL) : CertificateSigningService {
class HTTPNetworkRegistrationService(val server: URL) : NetworkRegistrationService {
companion object {
// TODO: Propagate version information from gradle
val clientVersion = "1.0"
}
@Throws(CertificateRequestException::class)
override fun retrieveCertificates(requestId: String): Array<Certificate>? {
// Poll server to download the signed certificate once request has been approved.
val url = URL("$server/api/certificate/$requestId")
@ -34,7 +35,7 @@ class HTTPCertificateSigningService(val server: URL) : CertificateSigningService
certificates.toTypedArray()
}
HTTP_NO_CONTENT -> null
HTTP_UNAUTHORIZED -> throw IOException("Certificate signing request has been rejected: ${conn.errorMessage}")
HTTP_UNAUTHORIZED -> throw CertificateRequestException("Certificate signing request has been rejected: ${conn.errorMessage}")
else -> throwUnexpectedResponseCode(conn)
}
}

View File

@ -0,0 +1,130 @@
package net.corda.node.utilities.registration
import net.corda.core.*
import net.corda.core.crypto.X509Utilities
import net.corda.core.crypto.X509Utilities.CORDA_CLIENT_CA
import net.corda.core.crypto.X509Utilities.CORDA_ROOT_CA
import net.corda.core.crypto.X509Utilities.addOrReplaceCertificate
import net.corda.core.crypto.X509Utilities.addOrReplaceKey
import net.corda.node.services.config.NodeConfiguration
import org.bouncycastle.openssl.jcajce.JcaPEMWriter
import org.bouncycastle.util.io.pem.PemObject
import java.io.StringWriter
import java.security.KeyPair
import java.security.cert.Certificate
import kotlin.system.exitProcess
/**
* This checks the [config.certificatesDirectory] for certificates required to connect to a Corda network.
* If the certificates are not found, a [PKCS10CertificationRequest] will be submitted to Corda network permissioning server using [NetworkRegistrationService].
* This process will enter a polling loop until the request has been approved, and then
* the certificate chain will be downloaded and stored in [Keystore] reside in [config.certificatesDirectory].
*/
class NetworkRegistrationHelper(val config: NodeConfiguration, val certService: NetworkRegistrationService) {
companion object {
val pollInterval = 10.seconds
val SELF_SIGNED_PRIVATE_KEY = "Self Signed Private Key"
}
private val requestIdStore = config.certificatesDirectory / "certificate-request-id.txt"
private val keystorePassword = config.keyStorePassword
// TODO: Use different password for private key.
private val privateKeyPassword = config.keyStorePassword
fun buildKeystore() {
config.certificatesDirectory.createDirectories()
val caKeyStore = X509Utilities.loadOrCreateKeyStore(config.keyStoreFile, keystorePassword)
if (!caKeyStore.containsAlias(CORDA_CLIENT_CA)) {
// Create or load self signed keypair from the key store.
// We use the self sign certificate to store the key temporarily in the keystore while waiting for the request approval.
if (!caKeyStore.containsAlias(SELF_SIGNED_PRIVATE_KEY)) {
val selfSignCert = X509Utilities.createSelfSignedCACert(config.myLegalName)
// Save to the key store.
caKeyStore.addOrReplaceKey(SELF_SIGNED_PRIVATE_KEY, selfSignCert.keyPair.private, privateKeyPassword.toCharArray(), arrayOf(selfSignCert.certificate))
X509Utilities.saveKeyStore(caKeyStore, config.keyStoreFile, keystorePassword)
}
val keyPair = X509Utilities.loadKeyPairFromKeyStore(config.keyStoreFile, keystorePassword, privateKeyPassword, SELF_SIGNED_PRIVATE_KEY)
val requestId = submitOrResumeCertificateSigningRequest(keyPair)
val certificates = try {
pollServerForCertificates(requestId)
} catch (e: CertificateRequestException) {
System.err.println(e.message)
println("Please make sure the details in configuration file are correct and try again.")
println("Corda node will now terminate.")
requestIdStore.deleteIfExists()
exitProcess(1)
}
println("Certificate signing request approved, storing private key with the certificate chain.")
// Save private key and certificate chain to the key store.
caKeyStore.addOrReplaceKey(CORDA_CLIENT_CA, keyPair.private, privateKeyPassword.toCharArray(), certificates)
caKeyStore.deleteEntry(SELF_SIGNED_PRIVATE_KEY)
X509Utilities.saveKeyStore(caKeyStore, config.keyStoreFile, keystorePassword)
// Save root certificates to trust store.
val trustStore = X509Utilities.loadOrCreateKeyStore(config.trustStoreFile, config.trustStorePassword)
// Assumes certificate chain always starts with client certificate and end with root certificate.
trustStore.addOrReplaceCertificate(CORDA_ROOT_CA, certificates.last())
X509Utilities.saveKeyStore(trustStore, config.trustStoreFile, config.trustStorePassword)
println("Certificate and private key stored in ${config.keyStoreFile}.")
// All done, clean up temp files.
requestIdStore.deleteIfExists()
} else {
println("Certificate already exists, Corda node will now terminate...")
}
}
/**
* Poll Certificate Signing Server for approved certificate,
* enter a slow polling loop if server return null.
* @param requestId Certificate signing request ID.
* @return Map of certificate chain.
*/
private fun pollServerForCertificates(requestId: String): Array<Certificate> {
println("Start polling server for certificate signing approval.")
// Poll server to download the signed certificate once request has been approved.
var certificates = certService.retrieveCertificates(requestId)
while (certificates == null) {
Thread.sleep(pollInterval.toMillis())
certificates = certService.retrieveCertificates(requestId)
}
return certificates
}
/**
* Submit Certificate Signing Request to Certificate signing service if request ID not found in file system
* New request ID will be stored in requestId.txt
* @param keyPair Public Private key pair generated for SSL certification.
* @return Request ID return from the server.
*/
private fun submitOrResumeCertificateSigningRequest(keyPair: KeyPair): String {
// Retrieve request id from file if exists, else post a request to server.
return if (!requestIdStore.exists()) {
val request = X509Utilities.createCertificateSigningRequest(config.myLegalName, config.nearestCity, config.emailAddress, keyPair)
val writer = StringWriter()
JcaPEMWriter(writer).use {
it.writeObject(PemObject("CERTIFICATE REQUEST", request.encoded))
}
println("Certificate signing request with the following information will be submitted to the Corda certificate signing server.")
println()
println("Legal Name: ${config.myLegalName}")
println("Nearest City: ${config.nearestCity}")
println("Email: ${config.emailAddress}")
println()
println("Public Key: ${keyPair.public}")
println()
println("$writer")
// Post request to signing server via http.
println("Submitting certificate signing request to Corda certificate signing server.")
val requestId = certService.submitRequest(request)
// Persists request ID to file in case of node shutdown.
requestIdStore.writeLines(listOf(requestId))
println("Successfully submitted request to Corda certificate signing server, request ID: $requestId.")
requestId
} else {
val requestId = requestIdStore.readLines { it.findFirst().get() }
println("Resuming from previous certificate signing request, request ID: $requestId.")
requestId
}
}
}

View File

@ -1,12 +1,15 @@
package net.corda.node.utilities.certsigning
package net.corda.node.utilities.registration
import org.bouncycastle.pkcs.PKCS10CertificationRequest
import java.security.cert.Certificate
interface CertificateSigningService {
interface NetworkRegistrationService {
/** Submits a CSR to the signing service and returns an opaque request ID. */
fun submitRequest(request: PKCS10CertificationRequest): String
/** Poll Certificate Signing Server for the request and returns a chain of certificates if request has been approved, null otherwise. */
@Throws(CertificateRequestException::class)
fun retrieveCertificates(requestId: String): Array<Certificate>?
}
class CertificateRequestException(message: String) : Exception(message)

View File

@ -20,7 +20,8 @@ class ArgsParserTest {
help = false,
logToConsole = false,
loggingLevel = Level.INFO,
isWebserver = false))
isWebserver = false,
isRegistration = false))
}
@Test

View File

@ -1,13 +1,11 @@
package net.corda.node.utilities.certsigning
package net.corda.node.utilities.registration
import com.nhaarman.mockito_kotlin.any
import com.nhaarman.mockito_kotlin.eq
import com.nhaarman.mockito_kotlin.mock
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.X509Utilities
import net.corda.core.div
import net.corda.core.exists
import net.corda.core.readLines
import net.corda.testing.TestNodeConfiguration
import org.junit.Rule
import org.junit.Test
@ -16,7 +14,7 @@ import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class CertificateSignerTest {
class NetworkRegistrationHelperTest {
@Rule
@JvmField
val tempFolder = TemporaryFolder()
@ -29,7 +27,7 @@ class CertificateSignerTest {
X509Utilities.createSelfSignedCACert("CORDA_INTERMEDIATE_CA").certificate,
X509Utilities.createSelfSignedCACert("CORDA_ROOT_CA").certificate)
val certService: CertificateSigningService = mock {
val certService: NetworkRegistrationService = mock {
on { submitRequest(any()) }.then { id }
on { retrieveCertificates(eq(id)) }.then { certs }
}
@ -42,13 +40,15 @@ class CertificateSignerTest {
assertFalse(config.keyStoreFile.exists())
assertFalse(config.trustStoreFile.exists())
CertificateSigner(config, certService).buildKeyStore()
NetworkRegistrationHelper(config, certService).buildKeystore()
assertTrue(config.keyStoreFile.exists())
assertTrue(config.trustStoreFile.exists())
X509Utilities.loadKeyStore(config.keyStoreFile, config.keyStorePassword).run {
assertTrue(containsAlias(X509Utilities.CORDA_CLIENT_CA_PRIVATE_KEY))
assertFalse(containsAlias(X509Utilities.CORDA_CLIENT_CA_PRIVATE_KEY))
val certificateChain = getCertificateChain(X509Utilities.CORDA_CLIENT_CA)
assertEquals(3, certificateChain.size)
assertTrue(containsAlias(X509Utilities.CORDA_CLIENT_CA))
assertFalse(containsAlias(X509Utilities.CORDA_INTERMEDIATE_CA))
assertFalse(containsAlias(X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY))
@ -64,7 +64,5 @@ class CertificateSignerTest {
assertTrue(containsAlias(X509Utilities.CORDA_ROOT_CA))
assertFalse(containsAlias(X509Utilities.CORDA_ROOT_CA_PRIVATE_KEY))
}
assertEquals(id, (config.certificatesDirectory / "certificate-request-id.txt").readLines { it.findFirst().get() })
}
}