mirror of
https://github.com/corda/corda.git
synced 2024-12-28 08:48:57 +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
@ -51,4 +51,46 @@ trustStorePassword = "password"
|
||||
|
||||
##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
|
||||
|
||||
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<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))
|
||||
.resolve()
|
||||
.parseAs<RegistrationConfig>()
|
||||
@ -48,7 +31,7 @@ fun main(args: Array<String>) {
|
||||
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<String>) {
|
||||
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,
|
||||
|
@ -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