diff --git a/network-management/registration-tool/README.md b/network-management/registration-tool/README.md index c64c110711..d80afb8981 100644 --- a/network-management/registration-tool/README.md +++ b/network-management/registration-tool/README.md @@ -51,4 +51,46 @@ trustStorePassword = "password" ##Running the registration tool -``java -jar registration-tool-<>.jar --config-file <>`` \ No newline at end of file +``java -jar registration-tool-<>.jar --config-file <>`` + +# Private key copy tool + +The key copy tool copies the private key from the source keystore to the destination keystore, it's similar to the ``importkeystore`` option in Java keytool with extra support for Corda's key algorithms. +**This is useful for provisioning keystores for distributed notaries.** + +### Command line option + +``` +Argument Description +--------- ----------- +srckeystore Path to the source keystore containing the private key. + +destkeystore Path to the destination keystore which the private key should copy to. + +srcstorepass Source keystore password. + +deststorepass Destination keystore password. + +srcalias The alias of the private key the tool is copying. + +destalias Optional: The private key will be stored using this alias if provided, otherwise [srcalias] will be used. + +``` + +### Usage + +``java -jar registration-tool-<>.jar --importkeystore [options]`` + +### Example + +Copy ``distributed-notary-private-key`` from distributed notaries keystore to node's keystore. + +``` +java -jar registration-tool-<>.jar \ +--importkeystore \ +--srckeystore notaryKeystore.jks \ +--destkeystore node.jks \ +--srcstorepass notaryPassword \ +--deststorepass nodepassword \ +--srcalias distributed-notary-private-key +``` diff --git a/network-management/registration-tool/src/main/kotlin/com/r3/corda/networkmanage/registration/KeyCopyTool.kt b/network-management/registration-tool/src/main/kotlin/com/r3/corda/networkmanage/registration/KeyCopyTool.kt new file mode 100644 index 0000000000..8bae09d6ff --- /dev/null +++ b/network-management/registration-tool/src/main/kotlin/com/r3/corda/networkmanage/registration/KeyCopyTool.kt @@ -0,0 +1,32 @@ +package com.r3.corda.networkmanage.registration + +import com.r3.corda.networkmanage.registration.ToolOption.KeyCopierOption +import net.corda.nodeapi.internal.crypto.X509KeyStore + +/** + * This utility will copy private key with [KeyCopierOption.srcAlias] from provided source keystore and copy it to target + * keystore with the same alias, or [KeyCopierOption.destAlias] if provided. + * + * This tool uses Corda's security provider which support EdDSA keys. + */ +fun KeyCopierOption.copyKeystore() { + println("**************************************") + println("* *") + println("* Key copy tool *") + println("* *") + println("**************************************") + println() + println("This utility will copy private key with [srcalias] from provided source keystore and copy it to target keystore with the same alias, or [destalias] if provided.") + println() + + // Read private key and certificates from notary identity keystore. + val srcKeystore = X509KeyStore.fromFile(srcPath, srcPass) + val srcPrivateKey = srcKeystore.getPrivateKey(srcAlias) + val srcCertChain = srcKeystore.getCertificateChain(srcAlias) + + X509KeyStore.fromFile(destPath, destPass).update { + val keyAlias = destAlias ?: srcAlias + setPrivateKey(keyAlias, srcPrivateKey, srcCertChain) + println("Added '$keyAlias' to keystore : $destPath, the tool will now terminate.") + } +} diff --git a/network-management/registration-tool/src/main/kotlin/com/r3/corda/networkmanage/registration/Main.kt b/network-management/registration-tool/src/main/kotlin/com/r3/corda/networkmanage/registration/Main.kt new file mode 100644 index 0000000000..f285f304b1 --- /dev/null +++ b/network-management/registration-tool/src/main/kotlin/com/r3/corda/networkmanage/registration/Main.kt @@ -0,0 +1,96 @@ +package com.r3.corda.networkmanage.registration + +import com.r3.corda.networkmanage.registration.ToolOption.KeyCopierOption +import com.r3.corda.networkmanage.registration.ToolOption.RegistrationOption +import joptsimple.ArgumentAcceptingOptionSpec +import joptsimple.OptionParser +import joptsimple.OptionSet +import joptsimple.OptionSpecBuilder +import joptsimple.util.PathConverter +import joptsimple.util.PathProperties +import java.nio.file.Path + +fun main(args: Array) { + val options = parseOptions(*args) + when (options) { + is RegistrationOption -> options.runRegistration() + is KeyCopierOption -> options.copyKeystore() + } +} + +private const val importKeyFlag = "importkeystore" + +internal fun parseOptions(vararg args: String): ToolOption { + val optionParser = OptionParser() + val isCopyKeyArg = optionParser.accepts(importKeyFlag) + + val configFileArg = optionParser + .accepts("config-file", "The path to the registration config file") + .availableUnless(importKeyFlag) + .withRequiredArg() + .withValuesConvertedBy(PathConverter(PathProperties.FILE_EXISTING)) + + // key copy tool args + val destKeystorePathArg = optionParser.accepts("destkeystore") + .requireOnlyIf(importKeyFlag) + .withRequiredArg() + .withValuesConvertedBy(PathConverter(PathProperties.FILE_EXISTING)) + val srcKeystorePathArg = optionParser.accepts("srckeystore") + .requireOnlyIf(importKeyFlag) + .withRequiredArg() + .withValuesConvertedBy(PathConverter(PathProperties.FILE_EXISTING)) + + val destPasswordArg = optionParser.accepts("deststorepass") + .requireOnlyIf(importKeyFlag) + .withRequiredArg() + val srcPasswordArg = optionParser.accepts("srcstorepass") + .requireOnlyIf(importKeyFlag) + .withRequiredArg() + + val destAliasArg = optionParser.accepts("destalias") + .availableIf(importKeyFlag) + .withRequiredArg() + val srcAliasArg = optionParser.accepts("srcalias") + .requireOnlyIf(importKeyFlag) + .withRequiredArg() + + val optionSet = optionParser.parse(*args) + val isCopyKey = optionSet.has(isCopyKeyArg) + + return if (isCopyKey) { + val targetKeystorePath = optionSet.valueOf(destKeystorePathArg) + val srcKeystorePath = optionSet.valueOf(srcKeystorePathArg) + val destPassword = optionSet.valueOf(destPasswordArg) + val srcPassword = optionSet.valueOf(srcPasswordArg) + val destAlias = optionSet.getOrNull(destAliasArg) + val srcAlias = optionSet.valueOf(srcAliasArg) + + KeyCopierOption(srcKeystorePath, targetKeystorePath, srcPassword, destPassword, srcAlias, destAlias) + } else { + val configFilePath = optionSet.valueOf(configFileArg) + RegistrationOption(configFilePath) + } +} + +private fun OptionSet.getOrNull(opt: ArgumentAcceptingOptionSpec): V? = if (has(opt)) valueOf(opt) else null + +private fun OptionSpecBuilder.requireOnlyIf(optionName: String): OptionSpecBuilder = requiredIf(optionName).availableIf(optionName) + +sealed class ToolOption { + data class RegistrationOption(val configFilePath: Path) : ToolOption() + data class KeyCopierOption(val srcPath: Path, + val destPath: Path, + val srcPass: String, + val destPass: String, + val srcAlias: String, + val destAlias: String?) : ToolOption() +} + +fun readPassword(fmt: String): String { + return if (System.console() != null) { + String(System.console().readPassword(fmt)) + } else { + print(fmt) + readLine() ?: "" + } +} diff --git a/network-management/registration-tool/src/main/kotlin/com/r3/corda/networkmanage/registration/RegistrationTool.kt b/network-management/registration-tool/src/main/kotlin/com/r3/corda/networkmanage/registration/RegistrationTool.kt index 723d82d71c..9216dfdb84 100644 --- a/network-management/registration-tool/src/main/kotlin/com/r3/corda/networkmanage/registration/RegistrationTool.kt +++ b/network-management/registration-tool/src/main/kotlin/com/r3/corda/networkmanage/registration/RegistrationTool.kt @@ -10,37 +10,20 @@ package com.r3.corda.networkmanage.registration +import com.r3.corda.networkmanage.registration.ToolOption.RegistrationOption import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigParseOptions -import joptsimple.OptionParser -import joptsimple.util.PathConverter import net.corda.core.identity.CordaX500Name import net.corda.core.internal.CertRole +import net.corda.core.internal.div import net.corda.node.utilities.registration.HTTPNetworkRegistrationService import net.corda.node.utilities.registration.NetworkRegistrationHelper import net.corda.nodeapi.internal.config.SSLConfiguration import net.corda.nodeapi.internal.config.parseAs import java.net.URL import java.nio.file.Path -import java.nio.file.Paths - -fun main(args: Array) { - val optionParser = OptionParser() - val configFileArg = optionParser - .accepts("config-file", "The path to the registration config file") - .withRequiredArg() - .withValuesConvertedBy(PathConverter()) - - val baseDirArg = optionParser - .accepts("baseDir", "The registration tool's base directory, default to current directory.") - .withRequiredArg() - .withValuesConvertedBy(PathConverter()) - .defaultsTo(Paths.get(".")) - - val optionSet = optionParser.parse(*args) - val configFilePath = optionSet.valueOf(configFileArg) - val baseDir = optionSet.valueOf(baseDirArg) +fun RegistrationOption.runRegistration() { val config = ConfigFactory.parseFile(configFilePath.toFile(), ConfigParseOptions.defaults().setAllowMissing(false)) .resolve() .parseAs() @@ -48,7 +31,7 @@ fun main(args: Array) { val sslConfig = object : SSLConfiguration { override val keyStorePassword: String by lazy { config.keyStorePassword ?: readPassword("Node Keystore password:") } override val trustStorePassword: String by lazy { config.trustStorePassword ?: readPassword("Node TrustStore password:") } - override val certificatesDirectory: Path = baseDir + override val certificatesDirectory: Path = configFilePath.parent / "certificates" } NetworkRegistrationHelper(sslConfig, @@ -59,15 +42,6 @@ fun main(args: Array) { config.networkRootTrustStorePassword ?: readPassword("Network trust root password:"), config.certRole).buildKeystore() } -fun readPassword(fmt: String): String { - return if (System.console() != null) { - String(System.console().readPassword(fmt)) - } else { - print(fmt) - readLine() ?: "" - } -} - data class RegistrationConfig(val legalName: CordaX500Name, val email: String, val compatibilityZoneURL: URL, diff --git a/network-management/registration-tool/src/test/kotlin/com/r3/corda/networkmanage/registration/KeyCopyToolTest.kt b/network-management/registration-tool/src/test/kotlin/com/r3/corda/networkmanage/registration/KeyCopyToolTest.kt new file mode 100644 index 0000000000..1ddab2a4f3 --- /dev/null +++ b/network-management/registration-tool/src/test/kotlin/com/r3/corda/networkmanage/registration/KeyCopyToolTest.kt @@ -0,0 +1,41 @@ +package com.r3.corda.networkmanage.registration + +import net.corda.core.crypto.Crypto +import net.corda.core.internal.div +import net.corda.nodeapi.internal.crypto.X509KeyStore +import net.corda.nodeapi.internal.crypto.X509Utilities +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.nio.file.Path +import javax.security.auth.x500.X500Principal + +class KeyCopyToolTest { + @Rule + @JvmField + val tempFolder = TemporaryFolder() + private val tempDir: Path by lazy { tempFolder.root.toPath() } + + @Test + fun `key copy correctly`() { + val keyCopyOption = ToolOption.KeyCopierOption(tempDir / "srcKeystore.jks", tempDir / "destKeystore.jks", "srctestpass", "desttestpass", "TestKeyAlias", null) + + // Prepare source and destination keystores + val keyPair = Crypto.generateKeyPair() + val cert = X509Utilities.createSelfSignedCACertificate(X500Principal("O=Test"), keyPair) + + X509KeyStore.fromFile(keyCopyOption.srcPath, keyCopyOption.srcPass, createNew = true).update { + setPrivateKey(keyCopyOption.srcAlias, keyPair.private, listOf(cert)) + } + X509KeyStore.fromFile(keyCopyOption.destPath, keyCopyOption.destPass, createNew = true) + + // Copy private key from src keystore to dest keystore using the tool + keyCopyOption.copyKeystore() + + // Verify key copied correctly + val destKeystore = X509KeyStore.fromFile(keyCopyOption.destPath, keyCopyOption.destPass) + assertEquals(keyPair.private, destKeystore.getPrivateKey(keyCopyOption.srcAlias, keyCopyOption.destPass)) + assertEquals(cert, destKeystore.getCertificate(keyCopyOption.srcAlias)) + } +} \ No newline at end of file diff --git a/network-management/registration-tool/src/test/kotlin/com/r3/corda/networkmanage/registration/OptionParserTest.kt b/network-management/registration-tool/src/test/kotlin/com/r3/corda/networkmanage/registration/OptionParserTest.kt new file mode 100644 index 0000000000..44c6bb446b --- /dev/null +++ b/network-management/registration-tool/src/test/kotlin/com/r3/corda/networkmanage/registration/OptionParserTest.kt @@ -0,0 +1,63 @@ +package com.r3.corda.networkmanage.registration + +import joptsimple.OptionException +import junit.framework.Assert.assertEquals +import net.corda.core.internal.div +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.nio.file.Files +import java.nio.file.Path + +class OptionParserTest { + @Rule + @JvmField + val tempFolder = TemporaryFolder() + lateinit var tempDir: Path + + @Before + fun setup() { + tempDir = tempFolder.root.toPath() + Files.createFile(tempDir / "test.file") + Files.createFile(tempDir / "source.jks") + Files.createFile(tempDir / "target.jks") + } + + @Test + fun `parse registration args correctly`() { + requireNotNull(parseOptions("--config-file", "${tempDir / "test.file"}") as? ToolOption.RegistrationOption).apply { + assertEquals(tempDir / "test.file", configFilePath) + } + } + + @Test + fun `registration args should be unavailable in key copy mode`() { + assertThatThrownBy { + val keyCopyArgs = arrayOf("--importkeystore", "--srckeystore", "${tempDir / "source.jks"}", "--srcstorepass", "password1", "--destkeystore", "${tempDir / "target.jks"}", "--deststorepass", "password2", "-srcalias", "testalias") + parseOptions(*keyCopyArgs, "--config-file", "test.file") + }.isInstanceOf(OptionException::class.java) + .hasMessageContaining("Option(s) [config-file] are unavailable given other options on the command line") + } + + @Test + fun `key copy args should be unavailable in registration mode`() { + assertThatThrownBy { + parseOptions("--config-file", "${tempDir / "test.file"}", "--srckeystore", "${tempDir / "source.jks"}") + }.isInstanceOf(OptionException::class.java) + .hasMessageContaining("Option(s) [srckeystore] are unavailable given other options on the command line") + } + + @Test + fun `parse key copy option correctly`() { + val keyCopyArgs = arrayOf("--importkeystore", "--srckeystore", "${tempDir / "source.jks"}", "--srcstorepass", "password1", "--destkeystore", "${tempDir / "target.jks"}", "--deststorepass", "password2", "-srcalias", "testalias") + requireNotNull(parseOptions(*keyCopyArgs) as? ToolOption.KeyCopierOption).apply { + assertEquals(tempDir / "source.jks", srcPath) + assertEquals(tempDir / "target.jks", destPath) + assertEquals("password1", srcPass) + assertEquals("password2", destPass) + assertEquals("testalias", srcAlias) + } + } +}