mirror of
https://github.com/corda/corda.git
synced 2025-03-15 00:36:49 +00:00
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:
parent
105adc9e8d
commit
5244d41384
@ -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',
|
||||
|
93
docs/source/ha-utilities.rst
Normal file
93
docs/source/ha-utilities.rst
Normal 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.
|
||||
|
@ -12,4 +12,5 @@ wish to try the :doc:`blob-inspector`.
|
||||
notary-healthcheck
|
||||
demobench
|
||||
node-explorer
|
||||
ha-utilities
|
||||
|
||||
|
@ -102,4 +102,5 @@ if (JavaVersion.current() == JavaVersion.VERSION_1_8) {
|
||||
include 'core-deterministic:testing:verifier'
|
||||
include 'serialization-deterministic'
|
||||
}
|
||||
include 'tools:ha-utilities'
|
||||
|
||||
|
40
tools/ha-utilities/build.gradle
Normal file
40
tools/ha-utilities/build.gradle
Normal 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'
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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() }
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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(), "_")
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
8
tools/ha-utilities/src/test/resources/nodeA.conf
Normal file
8
tools/ha-utilities/src/test/resources/nodeA.conf
Normal 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
|
8
tools/ha-utilities/src/test/resources/nodeB.conf
Normal file
8
tools/ha-utilities/src/test/resources/nodeB.conf
Normal 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
|
8
tools/ha-utilities/src/test/resources/nodeC.conf
Normal file
8
tools/ha-utilities/src/test/resources/nodeC.conf
Normal 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
|
Loading…
x
Reference in New Issue
Block a user