mirror of
https://github.com/corda/corda.git
synced 2024-12-30 17:57:02 +00:00
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:
parent
a99a910730
commit
2a898658c2
@ -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
|
||||||
|
```
|
||||||
|
@ -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.")
|
||||||
|
}
|
||||||
|
}
|
@ -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() ?: ""
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user