ENT-1561 - Identity copying tool for distributed notary (#563)

* check in before shelving this task

* keytool in-progress, TODO: Docs and more test

* unit test and doc

* minor typo
This commit is contained in:
Patrick Kuo 2018-03-16 09:42:27 +00:00 committed by GitHub
parent a99a910730
commit 2a898658c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 279 additions and 31 deletions

View File

@ -52,3 +52,45 @@ trustStorePassword = "password"
##Running the registration tool ##Running the registration tool
``java -jar registration-tool-<<version>>.jar --config-file <<config file path>>`` ``java -jar registration-tool-<<version>>.jar --config-file <<config file path>>``
# 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-<<version>>.jar --importkeystore [options]``
### Example
Copy ``distributed-notary-private-key`` from distributed notaries keystore to node's keystore.
```
java -jar registration-tool-<<version>>.jar \
--importkeystore \
--srckeystore notaryKeystore.jks \
--destkeystore node.jks \
--srcstorepass notaryPassword \
--deststorepass nodepassword \
--srcalias distributed-notary-private-key
```

View File

@ -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.")
}
}

View File

@ -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<String>) {
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 <V : Any> OptionSet.getOrNull(opt: ArgumentAcceptingOptionSpec<V>): 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() ?: ""
}
}

View File

@ -10,37 +10,20 @@
package com.r3.corda.networkmanage.registration package com.r3.corda.networkmanage.registration
import com.r3.corda.networkmanage.registration.ToolOption.RegistrationOption
import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigParseOptions import com.typesafe.config.ConfigParseOptions
import joptsimple.OptionParser
import joptsimple.util.PathConverter
import net.corda.core.identity.CordaX500Name import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.CertRole 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.HTTPNetworkRegistrationService
import net.corda.node.utilities.registration.NetworkRegistrationHelper import net.corda.node.utilities.registration.NetworkRegistrationHelper
import net.corda.nodeapi.internal.config.SSLConfiguration import net.corda.nodeapi.internal.config.SSLConfiguration
import net.corda.nodeapi.internal.config.parseAs import net.corda.nodeapi.internal.config.parseAs
import java.net.URL import java.net.URL
import java.nio.file.Path import java.nio.file.Path
import java.nio.file.Paths
fun main(args: Array<String>) {
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)) val config = ConfigFactory.parseFile(configFilePath.toFile(), ConfigParseOptions.defaults().setAllowMissing(false))
.resolve() .resolve()
.parseAs<RegistrationConfig>() .parseAs<RegistrationConfig>()
@ -48,7 +31,7 @@ fun main(args: Array<String>) {
val sslConfig = object : SSLConfiguration { val sslConfig = object : SSLConfiguration {
override val keyStorePassword: String by lazy { config.keyStorePassword ?: readPassword("Node Keystore password:") } 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 trustStorePassword: String by lazy { config.trustStorePassword ?: readPassword("Node TrustStore password:") }
override val certificatesDirectory: Path = baseDir override val certificatesDirectory: Path = configFilePath.parent / "certificates"
} }
NetworkRegistrationHelper(sslConfig, NetworkRegistrationHelper(sslConfig,
@ -59,15 +42,6 @@ fun main(args: Array<String>) {
config.networkRootTrustStorePassword ?: readPassword("Network trust root password:"), config.certRole).buildKeystore() 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, data class RegistrationConfig(val legalName: CordaX500Name,
val email: String, val email: String,
val compatibilityZoneURL: URL, val compatibilityZoneURL: URL,

View File

@ -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))
}
}

View File

@ -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)
}
}
}