diff --git a/build.gradle b/build.gradle index c4ea228d4f..712c08788d 100644 --- a/build.gradle +++ b/build.gradle @@ -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', diff --git a/docs/source/ha-utilities.rst b/docs/source/ha-utilities.rst new file mode 100644 index 0000000000..fdae2552a3 --- /dev/null +++ b/docs/source/ha-utilities.rst @@ -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| + +.. + +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=] [-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=``: 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=] [-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=``: 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=] [-b=] [-c=] [-l=] [-o=] [-p=] + +* ``-v``, ``--verbose``, ``--log-to-console``: If set, prints logging to the console as well as to a file. +* ``--logging-level=``: Enable logging at this level and higher. Possible values:ERROR, WARN, INFO, DEBUG, TRACE. Default: INFO +* ``-p``, ``--password=``: Default password for all generated keystore and private keys. Default: changeit +* ``-o``, ``--organization=``: X500Name's organization attribute. Default: Corda +* ``-l``, ``--locality=``: X500Name's locality attribute. Default: London +* ``-c``, ``--county=``: X500Name's country attribute. Default: GB +* ``-b``, ``--base-directory=``: 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. + diff --git a/docs/source/tools-index.rst b/docs/source/tools-index.rst index b94e4dff91..fc78a32716 100644 --- a/docs/source/tools-index.rst +++ b/docs/source/tools-index.rst @@ -12,4 +12,5 @@ wish to try the :doc:`blob-inspector`. notary-healthcheck demobench node-explorer + ha-utilities diff --git a/settings.gradle b/settings.gradle index 063afd51bd..26a9cfde4d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -102,4 +102,5 @@ if (JavaVersion.current() == JavaVersion.VERSION_1_8) { include 'core-deterministic:testing:verifier' include 'serialization-deterministic' } +include 'tools:ha-utilities' diff --git a/tools/ha-utilities/build.gradle b/tools/ha-utilities/build.gradle new file mode 100644 index 0000000000..82a575bee5 --- /dev/null +++ b/tools/ha-utilities/build.gradle @@ -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' +} diff --git a/tools/ha-utilities/src/main/kotlin/com/r3/ha/utilities/BridgeSSLKeyTool.kt b/tools/ha-utilities/src/main/kotlin/com/r3/ha/utilities/BridgeSSLKeyTool.kt new file mode 100644 index 0000000000..b8e535f78d --- /dev/null +++ b/tools/ha-utilities/src/main/kotlin/com/r3/ha/utilities/BridgeSSLKeyTool.kt @@ -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 + @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 + @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 + } +} \ No newline at end of file diff --git a/tools/ha-utilities/src/main/kotlin/com/r3/ha/utilities/InternalKeystoreGenerator.kt b/tools/ha-utilities/src/main/kotlin/com/r3/ha/utilities/InternalKeystoreGenerator.kt new file mode 100644 index 0000000000..34b0c33018 --- /dev/null +++ b/tools/ha-utilities/src/main/kotlin/com/r3/ha/utilities/InternalKeystoreGenerator.kt @@ -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() } + } +} \ No newline at end of file diff --git a/tools/ha-utilities/src/main/kotlin/com/r3/ha/utilities/Main.kt b/tools/ha-utilities/src/main/kotlin/com/r3/ha/utilities/Main.kt new file mode 100644 index 0000000000..a0067370b7 --- /dev/null +++ b/tools/ha-utilities/src/main/kotlin/com/r3/ha/utilities/Main.kt @@ -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) { + 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 + } +} \ No newline at end of file diff --git a/tools/ha-utilities/src/main/kotlin/com/r3/ha/utilities/RegistrationTool.kt b/tools/ha-utilities/src/main/kotlin/com/r3/ha/utilities/RegistrationTool.kt new file mode 100644 index 0000000000..ab0213ee11 --- /dev/null +++ b/tools/ha-utilities/src/main/kotlin/com/r3/ha/utilities/RegistrationTool.kt @@ -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 + @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(), "_") + } +} \ No newline at end of file diff --git a/tools/ha-utilities/src/test/kotlin/com/r3/ha/utilities/BridgeToolTest.kt b/tools/ha-utilities/src/test/kotlin/com/r3/ha/utilities/BridgeToolTest.kt new file mode 100644 index 0000000000..a18add27f3 --- /dev/null +++ b/tools/ha-utilities/src/test/kotlin/com/r3/ha/utilities/BridgeToolTest.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/tools/ha-utilities/src/test/kotlin/com/r3/ha/utilities/InternalKeystoreGeneratorTest.kt b/tools/ha-utilities/src/test/kotlin/com/r3/ha/utilities/InternalKeystoreGeneratorTest.kt new file mode 100644 index 0000000000..e5dc5ca1c1 --- /dev/null +++ b/tools/ha-utilities/src/test/kotlin/com/r3/ha/utilities/InternalKeystoreGeneratorTest.kt @@ -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)) + } + } +} \ No newline at end of file diff --git a/tools/ha-utilities/src/test/kotlin/com/r3/ha/utilities/RegistrationServer.kt b/tools/ha-utilities/src/test/kotlin/com/r3/ha/utilities/RegistrationServer.kt new file mode 100644 index 0000000000..0776104577 --- /dev/null +++ b/tools/ha-utilities/src/test/kotlin/com/r3/ha/utilities/RegistrationServer.kt @@ -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() + val certificates = mutableMapOf() + + @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() + } + } +} \ No newline at end of file diff --git a/tools/ha-utilities/src/test/kotlin/com/r3/ha/utilities/RegistrationToolTest.kt b/tools/ha-utilities/src/test/kotlin/com/r3/ha/utilities/RegistrationToolTest.kt new file mode 100644 index 0000000000..ada042724e --- /dev/null +++ b/tools/ha-utilities/src/test/kotlin/com/r3/ha/utilities/RegistrationToolTest.kt @@ -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()) + } +} diff --git a/tools/ha-utilities/src/test/resources/nodeA.conf b/tools/ha-utilities/src/test/resources/nodeA.conf new file mode 100644 index 0000000000..9ecf6f0607 --- /dev/null +++ b/tools/ha-utilities/src/test/resources/nodeA.conf @@ -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 \ No newline at end of file diff --git a/tools/ha-utilities/src/test/resources/nodeB.conf b/tools/ha-utilities/src/test/resources/nodeB.conf new file mode 100644 index 0000000000..cdd5b9997c --- /dev/null +++ b/tools/ha-utilities/src/test/resources/nodeB.conf @@ -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 \ No newline at end of file diff --git a/tools/ha-utilities/src/test/resources/nodeC.conf b/tools/ha-utilities/src/test/resources/nodeC.conf new file mode 100644 index 0000000000..06df1df890 --- /dev/null +++ b/tools/ha-utilities/src/test/resources/nodeC.conf @@ -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 \ No newline at end of file