ENT-2653 Standalone Keytool/Registration tool for HA deployment (#1558)

* Node registration tool for registering multiple nodes at the same time
* SSL key import tool for creating SSL keystore for the bridge or adding new key to existing bridge keystore
* Self signed SSL keystores generator for creating SSL keystores for firewall components' internal communication
This commit is contained in:
Patrick Kuo 2018-11-16 11:49:21 +00:00 committed by GitHub
parent 105adc9e8d
commit 5244d41384
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 674 additions and 0 deletions

View File

@ -398,6 +398,7 @@ bintrayConfig {
'corda-tools-explorer',
'corda-tools-network-bootstrapper',
'corda-tools-health-survey',
'corda-tools-ha-utilities',
'corda-firewall',
'corda-ptflows',
'jmeter-corda',

View File

@ -0,0 +1,93 @@
HA Utilities
============
Setting up multiple nodes behind shared Corda Firewall require preparation of various keystores and config files, which can be time consuming and error prone.
The HA Utilities aims to provide tools to streamline the node provision and deployment process.
The tool is distributed as part of |release| in the form of runnable JAR "|jar_name|".
.. |jar_name| replace:: corda-tools-ha-utilities-|version|.jar
To run simply pass in the file or URL as the first parameter:
.. parsed-literal::
> java -jar |jar_name| <file or URL>
..
Use the ``--help`` flag for a full list of command line options.
Sub-commands
^^^^^^^^^^^^
``node-registration``: Corda registration tool for registering 1 or more node with the corda network, using provided node configuration.
``import-ssl-key``: Key copying tool for creating bridge SSL keystore or add new node SSL identity to existing bridge SSL keystore.
``generate-internal-ssl-keystores``: Generate self-signed root and SSL certificates for bridge, external artemis broker and float, for internal communication between the services.
``install-shell-extensions``: Install alias and autocompletion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info.
Node Registration Tool
----------------------
The registration tool can be used to register multiple Corda nodes with the network operator, it is useful when managing multiple identities and setting up multiple Corda nodes sharing Corda firewall infrastructures.
Command-line options
~~~~~~~~~~~~~~~~~~~~
.. code-block:: shell
ha-utilities node-registration [-hvV] [--logging-level=<loggingLevel>] [-b=FOLDER] -p=PASSWORD -t=FILE -f=FILE... [-f=FILE...]...
* ``-v``, ``--verbose``, ``--log-to-console``: If set, prints logging to the console as well as to a file.
* ``--logging-level=<loggingLevel>``: Enable logging at this level and higher. Possible values: ERROR, WARN, INFO, DEBUG, TRACE. Default: INFO
* ``-b``, ``--base-directory=FOLDER``: The node working directory where all the files are kept.
* ``-f``, ``--config-files=FILE...``: The path to the config file
* ``-t``, ``--network-root-truststore=FILE``: Network root trust store obtained from network operator.
* ``-p``, ``--network-root-truststore-password=PASSWORD``: Network root trust store password obtained from network operator.
* ``-h``, ``--help``: Show this help message and exit.
* ``-V``, ``--version``: Print version information and exit.
SSL key copier
--------------
When using shared external bridge, the bridge will need to have access to nodes' SSL key in order to establish connections to counterparties on behalf of the nodes.
The SSL key copier sub command can be used to provision the SSL keystore and add additional keys when adding more nodes to the shared infrastructure.
Command-line options
~~~~~~~~~~~~~~~~~~~~
.. code-block:: shell
ha-utilities import-ssl-key [-hvV] [--logging-level=<loggingLevel>] [-b=FOLDER] [-k=FILES] -p=PASSWORDS --node-keystore-passwords=PASSWORDS... [--node-keystore-passwords=PASSWORDS...]... --node-keystores=FILES... [--node-keystores=FILES...]...
* ``-v``, ``--verbose``, ``--log-to-console``: If set, prints logging to the console as well as to a file.
* ``--logging-level=<loggingLevel>``: Enable logging at this level and higher. Possible values: ERROR, WARN, INFO, DEBUG, TRACE. Default: INFO
* ``--node-keystores=FILES...``: The path to the node SSL keystore(s)
* ``--node-keystore-passwords=PASSWORDS...``: The password(s) of the node SSL keystore(s)
* ``-b``, ``--base-directory=FOLDER``: The working directory where all the files are kept.
* ``-k``, ``--bridge-keystore=FILES``: The path to the bridge SSL keystore.
* ``-p``, ``--bridge-keystore-password=PASSWORDS``: The password of the bridge SSL keystore.
* ``-h``, ``--help``: Show this help message and exit.
* ``-V``, ``--version`` :Print version information and exit.
Self signed internal SSL keystore
---------------------------------
TLS is used to ensure communications between firewall components are secured. This tool can be used to generate the required keystores if TLS cert signing infrastructure is not available within your organisation.
Command-line options
~~~~~~~~~~~~~~~~~~~~
.. code-block:: shell
ha-utilities generate-internal-ssl-keystores [-hvV] [--logging-level=<loggingLevel>] [-b=<baseDirectory>] [-c=<country>] [-l=<locality>] [-o=<organization>] [-p=<password>]
* ``-v``, ``--verbose``, ``--log-to-console``: If set, prints logging to the console as well as to a file.
* ``--logging-level=<loggingLevel>``: Enable logging at this level and higher. Possible values:ERROR, WARN, INFO, DEBUG, TRACE. Default: INFO
* ``-p``, ``--password=<password>``: Default password for all generated keystore and private keys. Default: changeit
* ``-o``, ``--organization=<organization>``: X500Name's organization attribute. Default: Corda
* ``-l``, ``--locality=<locality>``: X500Name's locality attribute. Default: London
* ``-c``, ``--county=<country>``: X500Name's country attribute. Default: GB
* ``-b``, ``--base-directory=<baseDirectory>``: The node working directory where all the files are kept.
* ``-h``, ``--help``: Show this help message and exit.
* ``-V``, ``--version``: Print version information and exit.

View File

@ -12,4 +12,5 @@ wish to try the :doc:`blob-inspector`.
notary-healthcheck
demobench
node-explorer
ha-utilities

View File

@ -102,4 +102,5 @@ if (JavaVersion.current() == JavaVersion.VERSION_1_8) {
include 'core-deterministic:testing:verifier'
include 'serialization-deterministic'
}
include 'tools:ha-utilities'

View File

@ -0,0 +1,40 @@
apply plugin: 'kotlin'
apply plugin: 'net.corda.plugins.publish-utils'
apply plugin: 'com.jfrog.artifactory'
description 'HA Utilities'
dependencies {
compile project(':node')
compile project(':tools:cliutils')
compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version"
testCompile(project(':test-utils')) {
exclude group: 'org.apache.logging.log4j', module: 'log4j-slf4j-impl'
}
testCompile(project(':test-cli'))
testCompile(project(':node-driver'))
}
processResources {
from file("$rootDir/config/dev/log4j2.xml")
}
jar {
from(configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }) {
exclude "META-INF/*.SF"
exclude "META-INF/*.DSA"
exclude "META-INF/*.RSA"
}
baseName = "ha-utilities"
manifest {
attributes(
'Main-Class': 'com.r3.ha.utilities.MainKt'
)
}
}
publish {
name 'corda-tools-ha-utilities'
}

View File

@ -0,0 +1,54 @@
package com.r3.ha.utilities
import net.corda.cliutils.CliWrapperBase
import net.corda.cliutils.ExitCodes
import net.corda.core.crypto.SecureHash
import net.corda.core.internal.div
import net.corda.core.internal.exists
import net.corda.nodeapi.internal.crypto.X509KeyStore
import net.corda.nodeapi.internal.crypto.X509Utilities
import picocli.CommandLine.Option
import java.nio.file.Path
import java.nio.file.Paths
class BridgeSSLKeyTool : CliWrapperBase("import-ssl-key", "Key copying tool for creating bridge SSL keystore or add new node SSL identity to existing bridge SSL keystore.") {
@Option(names = ["--node-keystores"], arity = "1..*", paramLabel = "FILES", description = ["The path to the node SSL keystore(s)"], required = true)
lateinit var nodeKeystore: Array<Path>
@Option(names = ["--node-keystore-passwords"], arity = "1..*", paramLabel = "PASSWORDS", description = ["The password(s) of the node SSL keystore(s)"], required = true)
lateinit var nodeKeystorePasswords: Array<String>
@Option(names = ["-b", "--base-directory"], paramLabel = "FOLDER", description = ["The working directory where all the files are kept."])
var baseDirectory: Path = Paths.get(".").toAbsolutePath().normalize()
@Option(names = ["-k", "--bridge-keystore"], paramLabel = "FILES", description = ["The path to the bridge SSL keystore."])
private var _bridgeKeystore: Path? = null
val bridgeKeystore: Path get() = _bridgeKeystore ?: (baseDirectory / "bridge.jks")
@Option(names = ["-p", "--bridge-keystore-password"], paramLabel = "PASSWORDS", description = ["The password of the bridge SSL keystore."], required = true)
lateinit var bridgeKeystorePassword: String
override fun runProgram(): Int {
if (!bridgeKeystore.exists()) {
println("Creating new bridge SSL keystore.")
} else {
println("Adding new entries to bridge SSL keystore")
}
X509KeyStore.fromFile(bridgeKeystore, bridgeKeystorePassword, true).update {
// Use the same password for all keystore is only one is provided
// TODO: allow enter password interactively?
val passwords = if (nodeKeystorePasswords.size == 1) MutableList(nodeKeystore.size) { nodeKeystorePasswords.first() }.toTypedArray() else nodeKeystorePasswords
require(passwords.size == nodeKeystore.size) { "Number of passwords doesn't match the number of keystores, got ${passwords.size} passwords for ${nodeKeystore.size} keystores." }
nodeKeystore.zip(passwords).forEach { (keystore, password) ->
val tlsKeystore = X509KeyStore.fromFile(keystore, password, createNew = false)
val tlsKey = tlsKeystore.getPrivateKey(X509Utilities.CORDA_CLIENT_TLS, password)
val certChain = tlsKeystore.getCertificateChain(X509Utilities.CORDA_CLIENT_TLS)
val nameHash = SecureHash.sha256(certChain.first().subjectX500Principal.toString())
// Key password need to be same as the keystore password
val alias = "${X509Utilities.CORDA_CLIENT_TLS}-$nameHash"
setPrivateKey(alias, tlsKey, certChain, bridgeKeystorePassword)
println("Added new SSL key with alias '$alias', for identity '${certChain.first().subjectX500Principal}'")
}
println("Finish adding keys to keystore '$bridgeKeystore', keystore contains ${aliases().asSequence().count()} entries.")
}
return ExitCodes.SUCCESS
}
}

View File

@ -0,0 +1,87 @@
package com.r3.ha.utilities
import net.corda.cliutils.CliWrapperBase
import net.corda.cliutils.ExitCodes
import net.corda.core.crypto.Crypto
import net.corda.core.internal.div
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
import net.corda.nodeapi.internal.crypto.CertificateType
import net.corda.nodeapi.internal.crypto.X509KeyStore
import net.corda.nodeapi.internal.crypto.X509Utilities
import picocli.CommandLine.Option
import sun.security.x509.X500Name
import java.nio.file.Path
import java.nio.file.Paths
import javax.security.auth.x500.X500Principal
class InternalKeystoreGenerator : CliWrapperBase("generate-internal-ssl-keystores", "Generate self-signed root and SSL certificates for bridge, external artemis broker and float, for internal communication between the services.") {
companion object {
private const val DEFAULT_PASSWORD = "changeit"
}
@Option(names = ["-b", "--base-directory"], description = ["The node working directory where all the files are kept."])
var baseDirectory: Path = Paths.get(".").toAbsolutePath().normalize()
// TODO: options to generate keys for different HA deployment mode?
@Option(names = ["-p", "--password"], description = ["Default password for all generated keystore and private keys."], defaultValue = DEFAULT_PASSWORD)
lateinit var password: String
@Option(names = ["-o", "--organization"], description = ["X500Name's organization attribute."], defaultValue = "Corda")
lateinit var organization: String
@Option(names = ["-u", "--organization-unit"], description = ["X500Name's organization unit attribute."], required = false)
var organizationUnit: String? = null
@Option(names = ["-l", "--locality"], description = ["X500Name's locality attribute."], defaultValue = "London")
lateinit var locality: String
@Option(names = ["-c", "--county"], description = ["X500Name's country attribute."], defaultValue = "GB")
lateinit var country: String
override fun runProgram(): Int {
// Create tunnel certs
val tunnelCertDir = baseDirectory / "tunnel"
val tunnelRoot = createRootKeystore("Internal Tunnel Root", tunnelCertDir / "tunnel-root.jks", tunnelCertDir / "tunnel-truststore.jks").getCertificateAndKeyPair(X509Utilities.CORDA_ROOT_CA, password)
createTLSKeystore("float", tunnelRoot, tunnelCertDir / "float.jks")
// Create artemis certs
val artemisCertDir = baseDirectory / "artemis"
val root = createRootKeystore("Internal Artemis Root", artemisCertDir / "artemis-root.jks", artemisCertDir / "artemis-truststore.jks").getCertificateAndKeyPair(X509Utilities.CORDA_ROOT_CA, password)
createTLSKeystore("bridge", root, artemisCertDir / "bridge.jks")
createTLSKeystore("artemis", root, artemisCertDir / "artemis.jks")
createTLSKeystore("artemis-client", root, artemisCertDir / "artemis-client.jks")
if (password == DEFAULT_PASSWORD) {
println("Password is defaulted to $DEFAULT_PASSWORD, please change the keystores password using java keytool.")
}
return ExitCodes.SUCCESS
}
private fun createRootKeystore(commonName: String, keystorePath: Path, trustStorePath: Path): X509KeyStore {
val key = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val rootCert = X509Utilities.createSelfSignedCACertificate(getX500Principal(commonName), key)
val keystore = X509KeyStore.fromFile(keystorePath, password, createNew = true)
keystore.update {
setPrivateKey(X509Utilities.CORDA_ROOT_CA, key.private, listOf(rootCert), password)
}
println("$commonName keystore created in $keystorePath.")
X509KeyStore.fromFile(trustStorePath, password, createNew = true).setCertificate(X509Utilities.CORDA_ROOT_CA, rootCert)
println("$commonName truststore created in $trustStorePath.")
return keystore
}
private fun createTLSKeystore(serviceName: String, root: CertificateAndKeyPair, keystorePath: Path) {
val key = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val cert = X509Utilities.createCertificate(CertificateType.TLS, root.certificate, root.keyPair, getX500Principal(serviceName), key.public)
X509KeyStore.fromFile(keystorePath, password, createNew = true).update {
setPrivateKey(X509Utilities.CORDA_CLIENT_TLS, key.private, listOf(cert, root.certificate), password)
}
println("Internal TLS keystore for '$serviceName' created in $keystorePath.")
}
private fun getX500Principal(commonName: String): X500Principal {
return if (organizationUnit == null) {
"CN=$commonName, O=$organization, L=$locality, C=$country"
} else {
"CN=$commonName, OU=$organizationUnit, O=$organization, L=$locality, C=$country"
}.let { X500Name(it).asX500Principal() }
}
}

View File

@ -0,0 +1,18 @@
package com.r3.ha.utilities
import net.corda.cliutils.CordaCliWrapper
import net.corda.cliutils.ExitCodes
import net.corda.cliutils.start
fun main(args: Array<String>) {
HAUtilities().start(args)
}
class HAUtilities : CordaCliWrapper("ha-utilities", "HA utilities contains tools to help setting up corda firewall services.") {
override fun additionalSubCommands() = setOf(RegistrationTool(), BridgeSSLKeyTool(), InternalKeystoreGenerator())
override fun runProgram(): Int {
printHelp()
return ExitCodes.FAILURE
}
}

View File

@ -0,0 +1,61 @@
package com.r3.ha.utilities
import com.typesafe.config.ConfigValueFactory
import net.corda.cliutils.CordaCliWrapper
import net.corda.cliutils.CordaVersionProvider
import net.corda.cliutils.ExitCodes
import net.corda.core.internal.PLATFORM_VERSION
import net.corda.core.internal.div
import net.corda.node.NodeRegistrationOption
import net.corda.node.VersionInfo
import net.corda.node.services.config.ConfigHelper
import net.corda.node.services.config.parseAsNodeConfiguration
import net.corda.node.utilities.registration.HTTPNetworkRegistrationService
import net.corda.node.utilities.registration.NodeRegistrationHelper
import picocli.CommandLine.Option
import java.nio.file.Path
import java.nio.file.Paths
import kotlin.concurrent.thread
class RegistrationTool : CordaCliWrapper("node-registration", "Corda registration tool for registering 1 or more node with the corda network, using provided node configuration.") {
companion object {
private val VERSION_INFO = VersionInfo(
PLATFORM_VERSION,
CordaVersionProvider.releaseVersion,
CordaVersionProvider.revision,
CordaVersionProvider.vendor)
}
@Option(names = ["-b", "--base-directory"], paramLabel = "FOLDER", description = ["The node working directory where all the files are kept."])
var baseDirectory: Path = Paths.get(".").toAbsolutePath().normalize()
@Option(names = ["--config-files", "-f"], arity = "1..*", paramLabel = "FILE", description = ["The path to the config file"], required = true)
lateinit var configFiles: List<Path>
@Option(names = ["-t", "--network-root-truststore"], paramLabel = "FILE", description = ["Network root trust store obtained from network operator."], required = true)
var networkRootTrustStorePath: Path = Paths.get(".").toAbsolutePath().normalize() / "network-root-truststore.jks"
@Option(names = ["-p", "--network-root-truststore-password"], paramLabel = "PASSWORD", description = ["Network root trust store password obtained from network operator."], required = true)
lateinit var networkRootTrustStorePassword: String
override fun runProgram(): Int {
return try {
configFiles.map {
thread {
val legalName = ConfigHelper.loadConfig(it.parent, it).parseAsNodeConfiguration().value().myLegalName
// Load the config again with modified base directory.
val folderName = if (legalName.commonName == null) legalName.organisation else "${legalName.commonName},${legalName.organisation}"
val baseDir = baseDirectory / folderName.toFileName()
with(ConfigHelper.loadConfig(it.parent, it).withValue("baseDirectory", ConfigValueFactory.fromAnyRef(baseDir.toString())).parseAsNodeConfiguration().value()) {
NodeRegistrationHelper(this, HTTPNetworkRegistrationService(networkServices!!, VERSION_INFO), NodeRegistrationOption(networkRootTrustStorePath, networkRootTrustStorePassword)).generateKeysAndRegister()
}
}
}.forEach(Thread::join)
ExitCodes.SUCCESS
} catch (e: Exception) {
e.printStackTrace()
ExitCodes.FAILURE
}
}
private fun String.toFileName(): String {
return replace("[^a-zA-Z0-9-_.]".toRegex(), "_")
}
}

View File

@ -0,0 +1,66 @@
package com.r3.ha.utilities
import net.corda.core.crypto.Crypto
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.div
import net.corda.nodeapi.internal.DEV_INTERMEDIATE_CA
import net.corda.nodeapi.internal.DEV_ROOT_CA
import net.corda.nodeapi.internal.crypto.CertificateType
import net.corda.nodeapi.internal.crypto.X509KeyStore
import net.corda.nodeapi.internal.crypto.X509Utilities
import org.assertj.core.api.Assertions
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import picocli.CommandLine
import java.nio.file.Path
import kotlin.test.assertEquals
class BridgeToolTest {
companion object {
private val PASSWORD = "password"
}
private val sslKeyTool = BridgeSSLKeyTool()
@Rule
@JvmField
val tempFolder: TemporaryFolder = TemporaryFolder()
@Test
fun `tool adds tls key to new bridge store`() {
val workingDirectory = tempFolder.root.toPath()
createTLSKeystore(CordaX500Name("NodeA", "London", "GB"), workingDirectory / "nodeA.jks")
createTLSKeystore(CordaX500Name("NodeB", "London", "GB"), workingDirectory / "nodeB.jks")
createTLSKeystore(CordaX500Name("NodeC", "London", "GB"), workingDirectory / "nodeC.jks")
createTLSKeystore(CordaX500Name("NodeD", "London", "GB"), workingDirectory / "nodeD.jks")
CommandLine.populateCommand(sslKeyTool, "--base-directory", workingDirectory.toString(),
"--bridge-keystore-password", PASSWORD,
"--node-keystores", (workingDirectory / "nodeA.jks").toString(), (workingDirectory / "nodeB.jks").toString(), (workingDirectory / "nodeC.jks").toString(), (workingDirectory / "nodeD.jks").toString(),
"--node-keystore-passwords", PASSWORD)
Assertions.assertThat(sslKeyTool.baseDirectory).isEqualTo(workingDirectory)
Assertions.assertThat(sslKeyTool.bridgeKeystore).isEqualTo(workingDirectory / "bridge.jks")
sslKeyTool.runProgram()
val keystore = X509KeyStore.fromFile(workingDirectory / "bridge.jks", PASSWORD, createNew = false)
assertEquals(4, keystore.aliases().asSequence().count())
}
private fun createTLSKeystore(name: CordaX500Name, path: Path) {
val nodeCAKey = Crypto.generateKeyPair()
val nodeCACert = X509Utilities.createCertificate(CertificateType.NODE_CA, DEV_INTERMEDIATE_CA.certificate, DEV_INTERMEDIATE_CA.keyPair, name.x500Principal, nodeCAKey.public)
val tlsKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val tlsCert = X509Utilities.createCertificate(CertificateType.TLS, nodeCACert, nodeCAKey, name.x500Principal, tlsKey.public)
val certChain = listOf(tlsCert, nodeCACert, DEV_INTERMEDIATE_CA.certificate, DEV_ROOT_CA.certificate)
X509KeyStore.fromFile(path, PASSWORD, createNew = true).update {
setPrivateKey(X509Utilities.CORDA_CLIENT_TLS, tlsKey.private, certChain, PASSWORD)
}
}
}

View File

@ -0,0 +1,61 @@
package com.r3.ha.utilities
import net.corda.core.internal.div
import net.corda.core.internal.exists
import net.corda.nodeapi.internal.crypto.X509KeyStore
import net.corda.nodeapi.internal.crypto.X509Utilities
import org.assertj.core.api.Assertions
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import picocli.CommandLine
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class InternalKeystoreGeneratorTest {
private val generator = InternalKeystoreGenerator()
@Rule
@JvmField
val tempFolder: TemporaryFolder = TemporaryFolder()
@Test
fun `generate keystores correctly`() {
val workingDirectory = tempFolder.root.toPath()
CommandLine.populateCommand(generator, "--base-directory", workingDirectory.toString())
Assertions.assertThat(generator.baseDirectory).isEqualTo(workingDirectory)
generator.runProgram()
listOf("float.jks").map { workingDirectory / "tunnel" / it }.forEach {
assertTrue(it.exists())
assertTrue(X509KeyStore.fromFile(it, generator.password).contains(X509Utilities.CORDA_CLIENT_TLS))
assertTrue(X509KeyStore.fromFile(it, generator.password).internal.isKeyEntry(X509Utilities.CORDA_CLIENT_TLS))
}
X509KeyStore.fromFile(workingDirectory / "tunnel" / "tunnel-root.jks", generator.password).update {
assertTrue(contains(X509Utilities.CORDA_ROOT_CA))
assertTrue(internal.isKeyEntry(X509Utilities.CORDA_ROOT_CA))
}
X509KeyStore.fromFile(workingDirectory / "tunnel" / "tunnel-truststore.jks", generator.password).update {
assertTrue(contains(X509Utilities.CORDA_ROOT_CA))
assertFalse(internal.isKeyEntry(X509Utilities.CORDA_ROOT_CA))
}
listOf("bridge.jks", "artemis.jks", "artemis-client.jks").map { workingDirectory / "artemis" / it }.forEach {
assertTrue(it.exists())
assertTrue(X509KeyStore.fromFile(it, generator.password).contains(X509Utilities.CORDA_CLIENT_TLS))
assertTrue(X509KeyStore.fromFile(it, generator.password).internal.isKeyEntry(X509Utilities.CORDA_CLIENT_TLS))
}
X509KeyStore.fromFile(workingDirectory / "artemis" / "artemis-root.jks", generator.password).update {
assertTrue(contains(X509Utilities.CORDA_ROOT_CA))
assertTrue(internal.isKeyEntry(X509Utilities.CORDA_ROOT_CA))
}
X509KeyStore.fromFile(workingDirectory / "artemis" / "artemis-truststore.jks", generator.password).update {
assertTrue(contains(X509Utilities.CORDA_ROOT_CA))
assertFalse(internal.isKeyEntry(X509Utilities.CORDA_ROOT_CA))
}
}
}

View File

@ -0,0 +1,108 @@
package com.r3.ha.utilities
import net.corda.core.crypto.SecureHash
import net.corda.core.internal.readFully
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.nodeapi.internal.DEV_INTERMEDIATE_CA
import net.corda.nodeapi.internal.DEV_ROOT_CA
import net.corda.nodeapi.internal.crypto.CertificateType
import net.corda.nodeapi.internal.crypto.X509Utilities
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
import org.eclipse.jetty.server.Server
import org.eclipse.jetty.server.ServerConnector
import org.eclipse.jetty.server.handler.HandlerCollection
import org.eclipse.jetty.servlet.ServletContextHandler
import org.eclipse.jetty.servlet.ServletHolder
import org.glassfish.jersey.server.ResourceConfig
import org.glassfish.jersey.servlet.ServletContainer
import java.io.ByteArrayOutputStream
import java.io.Closeable
import java.io.InputStream
import java.net.InetSocketAddress
import java.security.cert.X509Certificate
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
import javax.security.auth.x500.X500Principal
import javax.ws.rs.*
import javax.ws.rs.core.MediaType
import javax.ws.rs.core.Response
/**
* A simple registration web server implementing the "doorman" protocol using [X509Utilities].
* This server is intended for integration testing only.
*/
class RegistrationServer(hostAndPort: NetworkHostAndPort = NetworkHostAndPort("localhost", 0),
vararg additionalServices: Any) : Closeable {
private val server: Server
private val service = SimpleDoormanService()
init {
server = Server(InetSocketAddress(hostAndPort.host, hostAndPort.port)).apply {
handler = HandlerCollection().apply {
addHandler(ServletContextHandler().apply {
contextPath = "/"
val resourceConfig = ResourceConfig().apply {
// Add your API provider classes (annotated for JAX-RS) here
register(service)
additionalServices.forEach { register(it) }
}
val jerseyServlet = ServletHolder(ServletContainer(resourceConfig)).apply { initOrder = 0 } // Initialise at server start
addServlet(jerseyServlet, "/*")
})
}
}
}
fun start(): NetworkHostAndPort {
server.start()
// Wait until server is up to obtain the host and port.
while (!server.isStarted) {
Thread.sleep(500)
}
return server.connectors
.mapNotNull { it as? ServerConnector }
.first()
.let { NetworkHostAndPort(it.host, it.localPort) }
}
override fun close() {
server.stop()
}
@Path("certificate")
internal class SimpleDoormanService {
val csrMap = mutableMapOf<String, JcaPKCS10CertificationRequest>()
val certificates = mutableMapOf<String, X509Certificate>()
@POST
@Consumes(MediaType.APPLICATION_OCTET_STREAM)
@Produces(MediaType.TEXT_PLAIN)
fun submitRequest(input: InputStream): Response {
val csr = JcaPKCS10CertificationRequest(input.readFully())
val requestId = SecureHash.randomSHA256().toString()
csrMap[requestId] = csr
certificates[requestId] = X509Utilities.createCertificate(CertificateType.NODE_CA, DEV_INTERMEDIATE_CA.certificate, DEV_INTERMEDIATE_CA.keyPair, X500Principal(csr.subject.toString()), csr.publicKey)
return Response.ok(requestId).build()
}
@GET
@Path("{var}")
@Produces(MediaType.APPLICATION_OCTET_STREAM)
fun retrieveCert(@PathParam("var") requestId: String): Response {
val cert = requireNotNull(certificates[requestId])
val baos = ByteArrayOutputStream()
ZipOutputStream(baos).use { zip ->
val certificates = arrayListOf(cert, DEV_INTERMEDIATE_CA.certificate, DEV_ROOT_CA.certificate)
listOf(X509Utilities.CORDA_CLIENT_CA, X509Utilities.CORDA_INTERMEDIATE_CA, X509Utilities.CORDA_ROOT_CA).zip(certificates).forEach {
zip.putNextEntry(ZipEntry("${it.first}.cer"))
zip.write(it.second.encoded)
zip.closeEntry()
}
}
return Response.ok(baos.toByteArray())
.type("application/zip")
.header("Content-Disposition", "attachment; filename=\"certificates.zip\"").build()
}
}
}

View File

@ -0,0 +1,59 @@
package com.r3.ha.utilities
import net.corda.core.internal.copyTo
import net.corda.core.internal.div
import net.corda.core.internal.exists
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.nodeapi.internal.DEV_ROOT_CA
import net.corda.nodeapi.internal.crypto.X509KeyStore
import net.corda.nodeapi.internal.crypto.X509Utilities
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import picocli.CommandLine
import kotlin.test.assertTrue
class RegistrationToolTest {
@Rule
@JvmField
val tempFolder: TemporaryFolder = TemporaryFolder()
private val registrationTool = RegistrationTool()
@Test
fun `the tool can register multiple nodes at the same time`() {
val workingDirectory = tempFolder.root.toPath()
// create network trust root trust store
val trustStorePath = workingDirectory / "networkTrustRootStore.jks"
X509KeyStore.fromFile(trustStorePath, "password", true).update {
setCertificate(X509Utilities.CORDA_ROOT_CA, DEV_ROOT_CA.certificate)
}
javaClass.classLoader.getResourceAsStream("nodeA.conf").copyTo(workingDirectory / "nodeA.conf")
javaClass.classLoader.getResourceAsStream("nodeB.conf").copyTo(workingDirectory / "nodeB.conf")
javaClass.classLoader.getResourceAsStream("nodeC.conf").copyTo(workingDirectory / "nodeC.conf")
RegistrationServer(NetworkHostAndPort("localhost", 10000)).use {
it.start()
CommandLine.populateCommand(registrationTool, "--base-directory", workingDirectory.toString(),
"--network-root-truststore", trustStorePath.toString(),
"--network-root-truststore-password", "password",
"--config-files", (workingDirectory / "nodeA.conf").toString(), (workingDirectory / "nodeB.conf").toString(), (workingDirectory / "nodeC.conf").toString())
registrationTool.runProgram()
}
assertTrue((workingDirectory / "PartyA" / "certificates" / "sslkeystore.jks").exists())
assertTrue((workingDirectory / "PartyB" / "certificates" / "sslkeystore.jks").exists())
assertTrue((workingDirectory / "PartyC" / "certificates" / "sslkeystore.jks").exists())
assertTrue((workingDirectory / "PartyA" / "certificates" / "truststore.jks").exists())
assertTrue((workingDirectory / "PartyB" / "certificates" / "truststore.jks").exists())
assertTrue((workingDirectory / "PartyC" / "certificates" / "truststore.jks").exists())
assertTrue((workingDirectory / "PartyA" / "certificates" / "nodekeystore.jks").exists())
assertTrue((workingDirectory / "PartyB" / "certificates" / "nodekeystore.jks").exists())
assertTrue((workingDirectory / "PartyC" / "certificates" / "nodekeystore.jks").exists())
}
}

View File

@ -0,0 +1,8 @@
myLegalName = "O=PartyA,L=London,C=GB"
compatibilityZoneURL = "http://localhost:10000"
p2pAddress = "localhost:12005"
rpcSettings {
address = "localhost:10008"
adminAddress = "localhost:10048"
}
devMode=false

View File

@ -0,0 +1,8 @@
myLegalName = "O=PartyB,L=London,C=GB"
compatibilityZoneURL = "http://localhost:10000"
p2pAddress = "localhost:12005"
rpcSettings {
address = "localhost:10008"
adminAddress = "localhost:10048"
}
devMode=false

View File

@ -0,0 +1,8 @@
myLegalName = "O=PartyC,L=London,C=GB"
compatibilityZoneURL = "http://localhost:10000"
p2pAddress = "localhost:12005"
rpcSettings {
address = "localhost:10008"
adminAddress = "localhost:10048"
}
devMode=false