mirror of
https://github.com/corda/corda.git
synced 2025-06-16 06:08:13 +00:00
CORDA 2131 - Extend Network Bootstrapper to enable registration of Java Package Namespaces. (#4116)
* Package Ownership Network Parameters: add register / unregister CLI options to network bootstrapper. * Fix 2 failing unit tests. * Fix failing unit tests. * Added changelog, documentation and cosmetic changes. * Fixed exception message. * Address PR review feedback. * Fix typo. * Resolve conflicts. * Rebase, resolve conflicts and remove PackageOwner class. * Address latest PR review feedback. * Fix incorrect imports. * Fix broken JUnit * Add support for key store passwords including delimiter characters. * Updated and improved documentation. * Minor doc update. * Documentation changes following PR review feedback * Replace Bank Of Corda with Example CorDapp. Remove references to locally built network bootstrapper.
This commit is contained in:
@ -2,6 +2,7 @@ package net.corda.core.node
|
|||||||
|
|
||||||
import net.corda.core.CordaRuntimeException
|
import net.corda.core.CordaRuntimeException
|
||||||
import net.corda.core.KeepForDJVM
|
import net.corda.core.KeepForDJVM
|
||||||
|
import net.corda.core.crypto.toStringShort
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.node.services.AttachmentId
|
import net.corda.core.node.services.AttachmentId
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
@ -140,7 +141,7 @@ data class NetworkParameters(
|
|||||||
modifiedTime=$modifiedTime
|
modifiedTime=$modifiedTime
|
||||||
epoch=$epoch,
|
epoch=$epoch,
|
||||||
packageOwnership= {
|
packageOwnership= {
|
||||||
${packageOwnership.keys.joinToString()}}
|
${packageOwnership.entries.joinToString("\n ") { "$it.key -> ${it.value.toStringShort()}" }}
|
||||||
}
|
}
|
||||||
}"""
|
}"""
|
||||||
}
|
}
|
||||||
@ -172,7 +173,7 @@ class ZoneVersionTooLowException(message: String) : CordaRuntimeException(messag
|
|||||||
@CordaSerializable
|
@CordaSerializable
|
||||||
data class JavaPackageName(val name: String) {
|
data class JavaPackageName(val name: String) {
|
||||||
init {
|
init {
|
||||||
require(isPackageValid(name)) { "Attempting to whitelist illegal java package: $name" }
|
require(isPackageValid(name)) { "Invalid Java package name: $name" }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -182,7 +183,9 @@ data class JavaPackageName(val name: String) {
|
|||||||
* Note: The ownership check is ignoring case to prevent people from just releasing a jar with: "com.megaCorp.megatoken" and pretend they are MegaCorp.
|
* Note: The ownership check is ignoring case to prevent people from just releasing a jar with: "com.megaCorp.megatoken" and pretend they are MegaCorp.
|
||||||
* By making the check case insensitive, the node will require that the jar is signed by MegaCorp, so the attack fails.
|
* By making the check case insensitive, the node will require that the jar is signed by MegaCorp, so the attack fails.
|
||||||
*/
|
*/
|
||||||
fun owns(fullClassName: String) = fullClassName.startsWith("${name}.", ignoreCase = true)
|
fun owns(fullClassName: String) = fullClassName.startsWith("$name.", ignoreCase = true)
|
||||||
|
|
||||||
|
override fun toString() = name
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if a string is a legal Java package name.
|
// Check if a string is a legal Java package name.
|
||||||
|
@ -28,6 +28,10 @@ infix fun Int.exactAdd(b: Int): Int = Math.addExact(this, b)
|
|||||||
/** Like the + operator but throws [ArithmeticException] in case of integer overflow. */
|
/** Like the + operator but throws [ArithmeticException] in case of integer overflow. */
|
||||||
infix fun Long.exactAdd(b: Long): Long = Math.addExact(this, b)
|
infix fun Long.exactAdd(b: Long): Long = Math.addExact(this, b)
|
||||||
|
|
||||||
|
/** There is no special case function for filtering null values out of a map in the stdlib */
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun <K, V> Map<K, V?>.filterNotNullValues() = filterValues { it != null } as Map<K, V>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Usually you won't need this method:
|
* Usually you won't need this method:
|
||||||
* * If you're in a companion object, use [contextLogger]
|
* * If you're in a companion object, use [contextLogger]
|
||||||
|
@ -7,6 +7,9 @@ release, see :doc:`upgrade-notes`.
|
|||||||
Unreleased
|
Unreleased
|
||||||
----------
|
----------
|
||||||
|
|
||||||
|
* Introduced new optional network bootstrapper command line options (--register-package-owner, --unregister-package-owner)
|
||||||
|
to register/unregister a java package namespace with an associated owner in the network parameter packageOwnership whitelist.
|
||||||
|
|
||||||
* New "validate-configuration" sub-command to `corda.jar`, allowing to validate the actual node configuration without starting the node.
|
* New "validate-configuration" sub-command to `corda.jar`, allowing to validate the actual node configuration without starting the node.
|
||||||
|
|
||||||
* Introduced new optional network bootstrapper command line option (--minimum-platform-version) to set as a network parameter
|
* Introduced new optional network bootstrapper command line option (--minimum-platform-version) to set as a network parameter
|
||||||
|
@ -247,6 +247,54 @@ To give the following:
|
|||||||
|
|
||||||
.. note:: The whitelist can only ever be appended to. Once added a contract implementation can never be removed.
|
.. note:: The whitelist can only ever be appended to. Once added a contract implementation can never be removed.
|
||||||
|
|
||||||
|
Package namespace ownership
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
Package namespace ownership is a Corda security feature that allows a compatibility zone to give ownership of parts of the Java package namespace to registered users (e.g. CorDapp development organisations).
|
||||||
|
The exact mechanism used to claim a namespace is up to the zone operator. A typical approach would be to accept an SSL
|
||||||
|
certificate with the domain in it as proof of domain ownership, or to accept an email from that domain.
|
||||||
|
|
||||||
|
.. note:: Read more about *Package ownership* :doc:`here<design/data-model-upgrades/package-namespace-ownership>`.
|
||||||
|
|
||||||
|
A Java package namespace is case insensitive and cannot be a sub-package of an existing registered namespace.
|
||||||
|
See `Naming a Package <https://docs.oracle.com/javase/tutorial/java/package/namingpkgs.html>`_ and `Naming Conventions <https://www.oracle.com/technetwork/java/javase/documentation/codeconventions-135099.html#28840 for guidelines and conventions>`_ for guidelines on naming conventions.
|
||||||
|
|
||||||
|
Registration of a java package namespace requires creation of a signed certificate as generated by the
|
||||||
|
`Java keytool <https://docs.oracle.com/javase/8/docs/technotes/tools/windows/keytool.html>`_.
|
||||||
|
|
||||||
|
The following four items are passed as a semi-colon separated string to the ``--register-package-owner`` command:
|
||||||
|
|
||||||
|
1. Java package name (e.g `com.my_company` ).
|
||||||
|
2. Keystore file refers to the full path of the file containing the signed certificate.
|
||||||
|
3. Password refers to the key store password (not to be confused with the key password).
|
||||||
|
4. Alias refers to the name associated with a certificate containing the public key to be associated with the package namespace.
|
||||||
|
|
||||||
|
Let's use the `Example CorDapp <https://github.com/corda/cordapp-example>`_ to initialise a simple network, and then register and unregister a package namespace.
|
||||||
|
Checkout the Example CorDapp and follow the instructions to build it `here <https://docs.corda.net/tutorial-cordapp.html#building-the-example-cordapp>`_.
|
||||||
|
|
||||||
|
.. note:: You can point to any existing bootstrapped corda network (this will have the effect of updating the associated network parameters file).
|
||||||
|
|
||||||
|
1. Create a new public key to use for signing the java package namespace we wish to register:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
$JAVA_HOME/bin/keytool -genkeypair -keystore _teststore -storepass MyStorePassword -keyalg RSA -alias MyKeyAlias -keypass MyKeyPassword -dname "O=Alice Corp, L=Madrid, C=ES"
|
||||||
|
|
||||||
|
This will generate a key store file called ``_teststore`` in the current directory.
|
||||||
|
|
||||||
|
2. Register the package namespace to be claimed by the public key generated above:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
# Register the java package namespace using the bootstrapper tool
|
||||||
|
java -jar network-bootstrapper.jar --dir build/nodes --register-package-owner com.example;./_teststore;MyStorePassword;MyKeyAlias
|
||||||
|
|
||||||
|
3. Unregister the package namespace:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
# Unregister the java package namespace using the bootstrapper tool
|
||||||
|
java -jar network-bootstrapper.jar --dir build/nodes --unregister-package-owner com.example
|
||||||
|
|
||||||
Command-line options
|
Command-line options
|
||||||
--------------------
|
--------------------
|
||||||
@ -256,7 +304,10 @@ The network bootstrapper can be started with the following command-line options:
|
|||||||
.. code-block:: shell
|
.. code-block:: shell
|
||||||
|
|
||||||
bootstrapper [-hvV] [--no-copy] [--dir=<dir>] [--logging-level=<loggingLevel>]
|
bootstrapper [-hvV] [--no-copy] [--dir=<dir>] [--logging-level=<loggingLevel>]
|
||||||
[--minimum-platform-version=<minimumPlatformVersion>] [COMMAND]
|
[--minimum-platform-version=<minimumPlatformVersion>]
|
||||||
|
[--register-package-owner java-package-namespace=keystore-file:password:alias]
|
||||||
|
[--unregister-package-owner java-package-namespace]
|
||||||
|
[COMMAND]
|
||||||
|
|
||||||
* ``--dir=<dir>``: Root directory containing the node configuration files and CorDapp JARs that will form the test network.
|
* ``--dir=<dir>``: Root directory containing the node configuration files and CorDapp JARs that will form the test network.
|
||||||
It may also contain existing node directories. Defaults to the current directory.
|
It may also contain existing node directories. Defaults to the current directory.
|
||||||
@ -266,8 +317,11 @@ The network bootstrapper can be started with the following command-line options:
|
|||||||
* ``--help``, ``-h``: Show this help message and exit.
|
* ``--help``, ``-h``: Show this help message and exit.
|
||||||
* ``--version``, ``-V``: Print version information and exit.
|
* ``--version``, ``-V``: Print version information and exit.
|
||||||
* ``--minimum-platform-version``: The minimum platform version to use in the generated network-parameters.
|
* ``--minimum-platform-version``: The minimum platform version to use in the generated network-parameters.
|
||||||
|
* ``--register-package-owner``: Register a java package namespace with its owners public key.
|
||||||
|
* ``--unregister-package-owner``: Unregister a java package namespace.
|
||||||
|
|
||||||
Sub-commands
|
Sub-commands
|
||||||
^^^^^^^^^^^^
|
^^^^^^^^^^^^
|
||||||
|
|
||||||
``install-shell-extensions``: Install ``bootstrapper`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info.
|
``install-shell-extensions``: Install ``bootstrapper`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info.
|
||||||
|
|
||||||
|
@ -2,11 +2,13 @@ package net.corda.nodeapi.internal.network
|
|||||||
|
|
||||||
import com.typesafe.config.Config
|
import com.typesafe.config.Config
|
||||||
import com.typesafe.config.ConfigFactory
|
import com.typesafe.config.ConfigFactory
|
||||||
|
import net.corda.core.crypto.toStringShort
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.internal.*
|
import net.corda.core.internal.*
|
||||||
import net.corda.core.internal.concurrent.fork
|
import net.corda.core.internal.concurrent.fork
|
||||||
import net.corda.core.internal.concurrent.transpose
|
import net.corda.core.internal.concurrent.transpose
|
||||||
|
import net.corda.core.node.JavaPackageName
|
||||||
import net.corda.core.node.NetworkParameters
|
import net.corda.core.node.NetworkParameters
|
||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
import net.corda.core.node.NotaryInfo
|
import net.corda.core.node.NotaryInfo
|
||||||
@ -17,6 +19,7 @@ import net.corda.core.serialization.deserialize
|
|||||||
import net.corda.core.serialization.internal.SerializationEnvironment
|
import net.corda.core.serialization.internal.SerializationEnvironment
|
||||||
import net.corda.core.serialization.internal._contextSerializationEnv
|
import net.corda.core.serialization.internal._contextSerializationEnv
|
||||||
import net.corda.core.utilities.days
|
import net.corda.core.utilities.days
|
||||||
|
import net.corda.core.utilities.filterNotNullValues
|
||||||
import net.corda.core.utilities.getOrThrow
|
import net.corda.core.utilities.getOrThrow
|
||||||
import net.corda.core.utilities.seconds
|
import net.corda.core.utilities.seconds
|
||||||
import net.corda.nodeapi.internal.*
|
import net.corda.nodeapi.internal.*
|
||||||
@ -30,6 +33,7 @@ import net.corda.serialization.internal.amqp.amqpMagic
|
|||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.nio.file.StandardCopyOption.REPLACE_EXISTING
|
import java.nio.file.StandardCopyOption.REPLACE_EXISTING
|
||||||
|
import java.security.PublicKey
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
@ -168,14 +172,14 @@ internal constructor(private val initSerEnv: Boolean,
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Entry point for the tool */
|
/** Entry point for the tool */
|
||||||
fun bootstrap(directory: Path, copyCordapps: Boolean, minimumPlatformVersion: Int) {
|
fun bootstrap(directory: Path, copyCordapps: Boolean, minimumPlatformVersion: Int, packageOwnership : Map<JavaPackageName, PublicKey?> = emptyMap()) {
|
||||||
require(minimumPlatformVersion <= PLATFORM_VERSION) { "Minimum platform version cannot be greater than $PLATFORM_VERSION" }
|
require(minimumPlatformVersion <= PLATFORM_VERSION) { "Minimum platform version cannot be greater than $PLATFORM_VERSION" }
|
||||||
// Don't accidently include the bootstrapper jar as a CorDapp!
|
// Don't accidently include the bootstrapper jar as a CorDapp!
|
||||||
val bootstrapperJar = javaClass.location.toPath()
|
val bootstrapperJar = javaClass.location.toPath()
|
||||||
val cordappJars = directory.list { paths ->
|
val cordappJars = directory.list { paths ->
|
||||||
paths.filter { it.toString().endsWith(".jar") && !it.isSameAs(bootstrapperJar) && it.fileName.toString() != "corda.jar" }.toList()
|
paths.filter { it.toString().endsWith(".jar") && !it.isSameAs(bootstrapperJar) && it.fileName.toString() != "corda.jar" }.toList()
|
||||||
}
|
}
|
||||||
bootstrap(directory, cordappJars, copyCordapps, fromCordform = false, minimumPlatformVersion = minimumPlatformVersion)
|
bootstrap(directory, cordappJars, copyCordapps, fromCordform = false, minimumPlatformVersion = minimumPlatformVersion, packageOwnership = packageOwnership)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bootstrap(
|
private fun bootstrap(
|
||||||
@ -183,7 +187,8 @@ internal constructor(private val initSerEnv: Boolean,
|
|||||||
cordappJars: List<Path>,
|
cordappJars: List<Path>,
|
||||||
copyCordapps: Boolean,
|
copyCordapps: Boolean,
|
||||||
fromCordform: Boolean,
|
fromCordform: Boolean,
|
||||||
minimumPlatformVersion: Int = PLATFORM_VERSION
|
minimumPlatformVersion: Int = PLATFORM_VERSION,
|
||||||
|
packageOwnership : Map<JavaPackageName, PublicKey?> = emptyMap()
|
||||||
) {
|
) {
|
||||||
directory.createDirectories()
|
directory.createDirectories()
|
||||||
println("Bootstrapping local test network in $directory")
|
println("Bootstrapping local test network in $directory")
|
||||||
@ -223,7 +228,7 @@ internal constructor(private val initSerEnv: Boolean,
|
|||||||
val notaryInfos = gatherNotaryInfos(nodeInfoFiles, configs)
|
val notaryInfos = gatherNotaryInfos(nodeInfoFiles, configs)
|
||||||
println("Generating contract implementations whitelist")
|
println("Generating contract implementations whitelist")
|
||||||
val newWhitelist = generateWhitelist(existingNetParams, readExcludeWhitelist(directory), cordappJars.filter { !isSigned(it) }.map(contractsJarConverter))
|
val newWhitelist = generateWhitelist(existingNetParams, readExcludeWhitelist(directory), cordappJars.filter { !isSigned(it) }.map(contractsJarConverter))
|
||||||
val newNetParams = installNetworkParameters(notaryInfos, newWhitelist, existingNetParams, nodeDirs, minimumPlatformVersion)
|
val newNetParams = installNetworkParameters(notaryInfos, newWhitelist, existingNetParams, nodeDirs, minimumPlatformVersion, packageOwnership)
|
||||||
if (newNetParams != existingNetParams) {
|
if (newNetParams != existingNetParams) {
|
||||||
println("${if (existingNetParams == null) "New" else "Updated"} $newNetParams")
|
println("${if (existingNetParams == null) "New" else "Updated"} $newNetParams")
|
||||||
} else {
|
} else {
|
||||||
@ -355,17 +360,31 @@ internal constructor(private val initSerEnv: Boolean,
|
|||||||
whitelist: Map<String, List<AttachmentId>>,
|
whitelist: Map<String, List<AttachmentId>>,
|
||||||
existingNetParams: NetworkParameters?,
|
existingNetParams: NetworkParameters?,
|
||||||
nodeDirs: List<Path>,
|
nodeDirs: List<Path>,
|
||||||
minimumPlatformVersion: Int
|
minimumPlatformVersion: Int,
|
||||||
|
packageOwnership : Map<JavaPackageName, PublicKey?>
|
||||||
): NetworkParameters {
|
): NetworkParameters {
|
||||||
// TODO Add config for minimumPlatformVersion, maxMessageSize and maxTransactionSize
|
// TODO Add config for maxMessageSize and maxTransactionSize
|
||||||
val netParams = if (existingNetParams != null) {
|
val netParams = if (existingNetParams != null) {
|
||||||
if (existingNetParams.whitelistedContractImplementations == whitelist && existingNetParams.notaries == notaryInfos) {
|
if (existingNetParams.whitelistedContractImplementations == whitelist && existingNetParams.notaries == notaryInfos &&
|
||||||
|
existingNetParams.packageOwnership.entries.containsAll(packageOwnership.entries)) {
|
||||||
existingNetParams
|
existingNetParams
|
||||||
} else {
|
} else {
|
||||||
|
var updatePackageOwnership = mutableMapOf(*existingNetParams.packageOwnership.map { Pair(it.key,it.value) }.toTypedArray())
|
||||||
|
packageOwnership.forEach { key, value ->
|
||||||
|
if (value == null) {
|
||||||
|
if (updatePackageOwnership.remove(key) != null)
|
||||||
|
println("Unregistering package $key")
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (updatePackageOwnership.put(key, value) == null)
|
||||||
|
println("Registering package $key for owner ${value.toStringShort()}")
|
||||||
|
}
|
||||||
|
}
|
||||||
existingNetParams.copy(
|
existingNetParams.copy(
|
||||||
notaries = notaryInfos,
|
notaries = notaryInfos,
|
||||||
modifiedTime = Instant.now(),
|
modifiedTime = Instant.now(),
|
||||||
whitelistedContractImplementations = whitelist,
|
whitelistedContractImplementations = whitelist,
|
||||||
|
packageOwnership = updatePackageOwnership,
|
||||||
epoch = existingNetParams.epoch + 1
|
epoch = existingNetParams.epoch + 1
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -377,6 +396,7 @@ internal constructor(private val initSerEnv: Boolean,
|
|||||||
maxMessageSize = 10485760,
|
maxMessageSize = 10485760,
|
||||||
maxTransactionSize = 10485760,
|
maxTransactionSize = 10485760,
|
||||||
whitelistedContractImplementations = whitelist,
|
whitelistedContractImplementations = whitelist,
|
||||||
|
packageOwnership = packageOwnership.filterNotNullValues(),
|
||||||
epoch = 1,
|
epoch = 1,
|
||||||
eventHorizon = 30.days
|
eventHorizon = 30.days
|
||||||
)
|
)
|
||||||
|
@ -5,29 +5,28 @@ import net.corda.core.crypto.secureRandomBytes
|
|||||||
import net.corda.core.crypto.sha256
|
import net.corda.core.crypto.sha256
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.internal.*
|
import net.corda.core.internal.*
|
||||||
|
import net.corda.core.node.JavaPackageName
|
||||||
import net.corda.core.node.NetworkParameters
|
import net.corda.core.node.NetworkParameters
|
||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
import net.corda.node.services.config.NotaryConfig
|
import net.corda.node.services.config.NotaryConfig
|
||||||
import net.corda.nodeapi.internal.DEV_ROOT_CA
|
import net.corda.nodeapi.internal.DEV_ROOT_CA
|
||||||
import net.corda.nodeapi.internal.NODE_INFO_DIRECTORY
|
import net.corda.nodeapi.internal.NODE_INFO_DIRECTORY
|
||||||
import net.corda.core.internal.PLATFORM_VERSION
|
|
||||||
import net.corda.nodeapi.internal.SignedNodeInfo
|
import net.corda.nodeapi.internal.SignedNodeInfo
|
||||||
import net.corda.nodeapi.internal.config.parseAs
|
import net.corda.nodeapi.internal.config.parseAs
|
||||||
import net.corda.nodeapi.internal.config.toConfig
|
import net.corda.nodeapi.internal.config.toConfig
|
||||||
import net.corda.nodeapi.internal.network.NodeInfoFilesCopier.Companion.NODE_INFO_FILE_NAME_PREFIX
|
import net.corda.nodeapi.internal.network.NodeInfoFilesCopier.Companion.NODE_INFO_FILE_NAME_PREFIX
|
||||||
import net.corda.testing.core.ALICE_NAME
|
import net.corda.testing.core.*
|
||||||
import net.corda.testing.core.BOB_NAME
|
|
||||||
import net.corda.testing.core.DUMMY_NOTARY_NAME
|
|
||||||
import net.corda.testing.core.SerializationEnvironmentRule
|
|
||||||
import net.corda.testing.internal.createNodeInfoAndSigned
|
import net.corda.testing.internal.createNodeInfoAndSigned
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import org.junit.rules.ExpectedException
|
||||||
import org.junit.rules.TemporaryFolder
|
import org.junit.rules.TemporaryFolder
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
|
import java.security.PublicKey
|
||||||
import kotlin.streams.toList
|
import kotlin.streams.toList
|
||||||
|
|
||||||
class NetworkBootstrapperTest {
|
class NetworkBootstrapperTest {
|
||||||
@ -35,6 +34,10 @@ class NetworkBootstrapperTest {
|
|||||||
@JvmField
|
@JvmField
|
||||||
val tempFolder = TemporaryFolder()
|
val tempFolder = TemporaryFolder()
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
@JvmField
|
||||||
|
val expectedEx: ExpectedException = ExpectedException.none()
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
@JvmField
|
@JvmField
|
||||||
val testSerialization = SerializationEnvironmentRule()
|
val testSerialization = SerializationEnvironmentRule()
|
||||||
@ -208,6 +211,80 @@ class NetworkBootstrapperTest {
|
|||||||
assertThat(networkParameters.epoch).isEqualTo(2)
|
assertThat(networkParameters.epoch).isEqualTo(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val ALICE = TestIdentity(ALICE_NAME, 70)
|
||||||
|
private val BOB = TestIdentity(BOB_NAME, 80)
|
||||||
|
|
||||||
|
private val alicePackageName = JavaPackageName("com.example.alice")
|
||||||
|
private val bobPackageName = JavaPackageName("com.example.bob")
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `register new package namespace in existing network`() {
|
||||||
|
createNodeConfFile("alice", aliceConfig)
|
||||||
|
bootstrap(packageOwnership = mapOf(Pair(alicePackageName, ALICE.publicKey)))
|
||||||
|
assertContainsPackageOwner("alice", mapOf(Pair(alicePackageName, ALICE.publicKey)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `register additional package namespace in existing network`() {
|
||||||
|
createNodeConfFile("alice", aliceConfig)
|
||||||
|
bootstrap(packageOwnership = mapOf(Pair(alicePackageName, ALICE.publicKey)))
|
||||||
|
assertContainsPackageOwner("alice", mapOf(Pair(alicePackageName, ALICE.publicKey)))
|
||||||
|
// register additional package name
|
||||||
|
createNodeConfFile("bob", bobConfig)
|
||||||
|
bootstrap(packageOwnership = mapOf(Pair(bobPackageName, BOB.publicKey)))
|
||||||
|
assertContainsPackageOwner("bob", mapOf(Pair(alicePackageName, ALICE.publicKey), Pair(bobPackageName, BOB.publicKey)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `attempt to register overlapping namespaces in existing network`() {
|
||||||
|
createNodeConfFile("alice", aliceConfig)
|
||||||
|
val greedyNamespace = JavaPackageName("com.example")
|
||||||
|
bootstrap(packageOwnership = mapOf(Pair(greedyNamespace, ALICE.publicKey)))
|
||||||
|
assertContainsPackageOwner("alice", mapOf(Pair(greedyNamespace, ALICE.publicKey)))
|
||||||
|
// register overlapping package name
|
||||||
|
createNodeConfFile("bob", bobConfig)
|
||||||
|
expectedEx.expect(IllegalArgumentException::class.java)
|
||||||
|
expectedEx.expectMessage("multiple packages added to the packageOwnership overlap.")
|
||||||
|
bootstrap(packageOwnership = mapOf(Pair(bobPackageName, BOB.publicKey)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `unregister single package namespace in network of one`() {
|
||||||
|
createNodeConfFile("alice", aliceConfig)
|
||||||
|
bootstrap(packageOwnership = mapOf(Pair(alicePackageName, ALICE.publicKey)))
|
||||||
|
assertContainsPackageOwner("alice", mapOf(Pair(alicePackageName, ALICE.publicKey)))
|
||||||
|
// unregister package name
|
||||||
|
bootstrap(packageOwnership = mapOf(Pair(alicePackageName, null)))
|
||||||
|
assertContainsPackageOwner("alice", emptyMap())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `unregister single package namespace in network of many`() {
|
||||||
|
createNodeConfFile("alice", aliceConfig)
|
||||||
|
bootstrap(packageOwnership = mapOf(Pair(alicePackageName, ALICE.publicKey), Pair(bobPackageName, BOB.publicKey)))
|
||||||
|
// unregister package name
|
||||||
|
bootstrap(packageOwnership = mapOf(Pair(alicePackageName, null)))
|
||||||
|
assertContainsPackageOwner("alice", mapOf(Pair(bobPackageName, BOB.publicKey)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `unregister all package namespaces in existing network`() {
|
||||||
|
createNodeConfFile("alice", aliceConfig)
|
||||||
|
bootstrap(packageOwnership = mapOf(Pair(alicePackageName, ALICE.publicKey), Pair(bobPackageName, BOB.publicKey)))
|
||||||
|
// unregister all package names
|
||||||
|
bootstrap(packageOwnership = mapOf(Pair(alicePackageName, null), Pair(bobPackageName, null)))
|
||||||
|
assertContainsPackageOwner("alice", emptyMap())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `register and unregister sample package namespace in network`() {
|
||||||
|
createNodeConfFile("alice", aliceConfig)
|
||||||
|
bootstrap(packageOwnership = mapOf(Pair(alicePackageName, ALICE.publicKey), Pair(alicePackageName, null)))
|
||||||
|
assertContainsPackageOwner("alice", emptyMap())
|
||||||
|
bootstrap(packageOwnership = mapOf(Pair(alicePackageName, null), Pair(alicePackageName, ALICE.publicKey)))
|
||||||
|
assertContainsPackageOwner("alice", mapOf(Pair(alicePackageName, ALICE.publicKey)))
|
||||||
|
}
|
||||||
|
|
||||||
private val rootDir get() = tempFolder.root.toPath()
|
private val rootDir get() = tempFolder.root.toPath()
|
||||||
|
|
||||||
private fun fakeFileBytes(writeToFile: Path? = null): ByteArray {
|
private fun fakeFileBytes(writeToFile: Path? = null): ByteArray {
|
||||||
@ -216,9 +293,9 @@ class NetworkBootstrapperTest {
|
|||||||
return bytes
|
return bytes
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bootstrap(copyCordapps: Boolean = true) {
|
private fun bootstrap(copyCordapps: Boolean = true, packageOwnership : Map<JavaPackageName, PublicKey?> = emptyMap()) {
|
||||||
providedCordaJar = (rootDir / "corda.jar").let { if (it.exists()) it.readAll() else null }
|
providedCordaJar = (rootDir / "corda.jar").let { if (it.exists()) it.readAll() else null }
|
||||||
bootstrapper.bootstrap(rootDir, copyCordapps, PLATFORM_VERSION)
|
bootstrapper.bootstrap(rootDir, copyCordapps, PLATFORM_VERSION, packageOwnership)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createNodeConfFile(nodeDirName: String, config: FakeNodeConfig) {
|
private fun createNodeConfFile(nodeDirName: String, config: FakeNodeConfig) {
|
||||||
@ -286,5 +363,10 @@ class NetworkBootstrapperTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun assertContainsPackageOwner(nodeDirName: String, packageOwners: Map<JavaPackageName, PublicKey>) {
|
||||||
|
val networkParams = (rootDir / nodeDirName).networkParameters
|
||||||
|
assertThat(networkParams.packageOwnership).isEqualTo(packageOwners)
|
||||||
|
}
|
||||||
|
|
||||||
data class FakeNodeConfig(val myLegalName: CordaX500Name, val notary: NotaryConfig? = null)
|
data class FakeNodeConfig(val myLegalName: CordaX500Name, val notary: NotaryConfig? = null)
|
||||||
}
|
}
|
||||||
|
@ -107,7 +107,7 @@ class NetworkParametersTest {
|
|||||||
JavaPackageName("com.!example.stuff") to key2
|
JavaPackageName("com.!example.stuff") to key2
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}.withMessageContaining("Attempting to whitelist illegal java package")
|
}.withMessageContaining("Invalid Java package name")
|
||||||
|
|
||||||
assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy {
|
assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy {
|
||||||
NetworkParameters(1,
|
NetworkParameters(1,
|
||||||
|
@ -6,9 +6,6 @@ import com.google.common.jimfs.Configuration
|
|||||||
import com.google.common.jimfs.Jimfs
|
import com.google.common.jimfs.Jimfs
|
||||||
import com.nhaarman.mockito_kotlin.doReturn
|
import com.nhaarman.mockito_kotlin.doReturn
|
||||||
import com.nhaarman.mockito_kotlin.whenever
|
import com.nhaarman.mockito_kotlin.whenever
|
||||||
import net.corda.core.JarSignatureTestUtils.createJar
|
|
||||||
import net.corda.core.JarSignatureTestUtils.generateKey
|
|
||||||
import net.corda.core.JarSignatureTestUtils.signJar
|
|
||||||
import net.corda.core.contracts.ContractAttachment
|
import net.corda.core.contracts.ContractAttachment
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.crypto.sha256
|
import net.corda.core.crypto.sha256
|
||||||
@ -21,14 +18,17 @@ import net.corda.core.node.services.vault.Builder
|
|||||||
import net.corda.core.node.services.vault.Sort
|
import net.corda.core.node.services.vault.Sort
|
||||||
import net.corda.core.utilities.getOrThrow
|
import net.corda.core.utilities.getOrThrow
|
||||||
import net.corda.node.services.transactions.PersistentUniquenessProvider
|
import net.corda.node.services.transactions.PersistentUniquenessProvider
|
||||||
import net.corda.testing.internal.TestingNamedCacheFactory
|
|
||||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||||
import net.corda.testing.common.internal.testNetworkParameters
|
import net.corda.testing.common.internal.testNetworkParameters
|
||||||
import net.corda.testing.core.ALICE_NAME
|
import net.corda.testing.core.ALICE_NAME
|
||||||
|
import net.corda.testing.core.JarSignatureTestUtils.createJar
|
||||||
|
import net.corda.testing.core.JarSignatureTestUtils.generateKey
|
||||||
|
import net.corda.testing.core.JarSignatureTestUtils.signJar
|
||||||
import net.corda.testing.internal.LogHelper
|
import net.corda.testing.internal.LogHelper
|
||||||
import net.corda.testing.internal.rigorousMock
|
import net.corda.testing.internal.TestingNamedCacheFactory
|
||||||
import net.corda.testing.internal.configureDatabase
|
import net.corda.testing.internal.configureDatabase
|
||||||
|
import net.corda.testing.internal.rigorousMock
|
||||||
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
|
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
|
||||||
import net.corda.testing.node.internal.InternalMockNetwork
|
import net.corda.testing.node.internal.InternalMockNetwork
|
||||||
import net.corda.testing.node.internal.startFlow
|
import net.corda.testing.node.internal.startFlow
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package net.corda.core
|
package net.corda.testing.core
|
||||||
|
|
||||||
import net.corda.core.internal.JarSignatureCollector
|
import net.corda.core.internal.JarSignatureCollector
|
||||||
import net.corda.core.internal.div
|
import net.corda.core.internal.div
|
||||||
@ -25,8 +25,8 @@ object JarSignatureTestUtils {
|
|||||||
.waitFor())
|
.waitFor())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Path.generateKey(alias: String, password: String, name: String, keyalg: String = "RSA") =
|
fun Path.generateKey(alias: String, storePassword: String, name: String, keyalg: String = "RSA", keyPassword: String = storePassword, storeName: String = "_teststore") =
|
||||||
executeProcess("keytool", "-genkey", "-keystore", "_teststore", "-storepass", "storepass", "-keyalg", keyalg, "-alias", alias, "-keypass", password, "-dname", name)
|
executeProcess("keytool", "-genkeypair", "-keystore" ,storeName, "-storepass", storePassword, "-keyalg", keyalg, "-alias", alias, "-keypass", keyPassword, "-dname", name)
|
||||||
|
|
||||||
fun Path.createJar(fileName: String, vararg contents: String) =
|
fun Path.createJar(fileName: String, vararg contents: String) =
|
||||||
executeProcess(*(arrayOf("jar", "cvf", fileName) + contents))
|
executeProcess(*(arrayOf("jar", "cvf", fileName) + contents))
|
||||||
@ -34,9 +34,9 @@ object JarSignatureTestUtils {
|
|||||||
fun Path.updateJar(fileName: String, vararg contents: String) =
|
fun Path.updateJar(fileName: String, vararg contents: String) =
|
||||||
executeProcess(*(arrayOf("jar", "uvf", fileName) + contents))
|
executeProcess(*(arrayOf("jar", "uvf", fileName) + contents))
|
||||||
|
|
||||||
fun Path.signJar(fileName: String, alias: String, password: String): PublicKey {
|
fun Path.signJar(fileName: String, alias: String, storePassword: String, keyPassword: String = storePassword): PublicKey {
|
||||||
executeProcess("jarsigner", "-keystore", "_teststore", "-storepass", "storepass", "-keypass", password, fileName, alias)
|
executeProcess("jarsigner", "-keystore", "_teststore", "-storepass", storePassword, "-keypass", keyPassword, fileName, alias)
|
||||||
val ks = loadKeyStore(this.resolve("_teststore"), "storepass")
|
val ks = loadKeyStore(this.resolve("_teststore"), storePassword)
|
||||||
return ks.getCertificate(alias).publicKey
|
return ks.getCertificate(alias).publicKey
|
||||||
}
|
}
|
||||||
|
|
@ -1,14 +1,14 @@
|
|||||||
package net.corda.core.internal
|
package net.corda.testing.core
|
||||||
|
|
||||||
import net.corda.core.JarSignatureTestUtils.createJar
|
import net.corda.testing.core.JarSignatureTestUtils.createJar
|
||||||
import net.corda.core.JarSignatureTestUtils.generateKey
|
import net.corda.testing.core.JarSignatureTestUtils.generateKey
|
||||||
import net.corda.core.JarSignatureTestUtils.getJarSigners
|
import net.corda.testing.core.JarSignatureTestUtils.getJarSigners
|
||||||
import net.corda.core.JarSignatureTestUtils.signJar
|
import net.corda.testing.core.JarSignatureTestUtils.signJar
|
||||||
import net.corda.core.JarSignatureTestUtils.updateJar
|
import net.corda.testing.core.JarSignatureTestUtils.updateJar
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.core.internal.*
|
||||||
import net.corda.testing.core.ALICE_NAME
|
import net.corda.testing.core.ALICE_NAME
|
||||||
import net.corda.testing.core.BOB_NAME
|
import net.corda.testing.core.BOB_NAME
|
||||||
import net.corda.testing.core.CHARLIE_NAME
|
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.AfterClass
|
import org.junit.AfterClass
|
||||||
@ -34,8 +34,8 @@ class JarSignatureCollectorTest {
|
|||||||
@BeforeClass
|
@BeforeClass
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun beforeClass() {
|
fun beforeClass() {
|
||||||
dir.generateKey(ALICE, ALICE_PASS, ALICE_NAME.toString())
|
dir.generateKey(ALICE, "storepass", ALICE_NAME.toString(), keyPassword = ALICE_PASS)
|
||||||
dir.generateKey(BOB, BOB_PASS, BOB_NAME.toString())
|
dir.generateKey(BOB, "storepass", BOB_NAME.toString(), keyPassword = BOB_PASS)
|
||||||
|
|
||||||
(dir / "_signable1").writeLines(listOf("signable1"))
|
(dir / "_signable1").writeLines(listOf("signable1"))
|
||||||
(dir / "_signable2").writeLines(listOf("signable2"))
|
(dir / "_signable2").writeLines(listOf("signable2"))
|
||||||
@ -134,12 +134,12 @@ class JarSignatureCollectorTest {
|
|||||||
// and our JarSignatureCollector
|
// and our JarSignatureCollector
|
||||||
@Test
|
@Test
|
||||||
fun `one signer with EC algorithm`() {
|
fun `one signer with EC algorithm`() {
|
||||||
dir.generateKey(CHARLIE, CHARLIE_PASS, CHARLIE_NAME.toString(), "EC")
|
dir.generateKey(CHARLIE, "storepass", CHARLIE_NAME.toString(), "EC", CHARLIE_PASS)
|
||||||
dir.createJar(FILENAME, "_signable1", "_signable2")
|
dir.createJar(FILENAME, "_signable1", "_signable2")
|
||||||
val key = dir.signJar(FILENAME, CHARLIE, CHARLIE_PASS)
|
val key = dir.signJar(FILENAME, CHARLIE, "storepass", CHARLIE_PASS)
|
||||||
assertEquals(listOf(key), dir.getJarSigners(FILENAME)) // We only used CHARLIE's distinguished name, so the keys will be different.
|
assertEquals(listOf(key), dir.getJarSigners(FILENAME)) // We only used CHARLIE's distinguished name, so the keys will be different.
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun signAsAlice() = dir.signJar(FILENAME, ALICE, ALICE_PASS)
|
private fun signAsAlice() = dir.signJar(FILENAME, ALICE, "storepass", ALICE_PASS)
|
||||||
private fun signAsBob() = dir.signJar(FILENAME, BOB, BOB_PASS)
|
private fun signAsBob() = dir.signJar(FILENAME, BOB, "storepass", BOB_PASS)
|
||||||
}
|
}
|
@ -3,10 +3,16 @@ package net.corda.bootstrapper
|
|||||||
import net.corda.cliutils.CordaCliWrapper
|
import net.corda.cliutils.CordaCliWrapper
|
||||||
import net.corda.cliutils.start
|
import net.corda.cliutils.start
|
||||||
import net.corda.core.internal.PLATFORM_VERSION
|
import net.corda.core.internal.PLATFORM_VERSION
|
||||||
|
import net.corda.core.node.JavaPackageName
|
||||||
|
import net.corda.nodeapi.internal.crypto.loadKeyStore
|
||||||
import net.corda.nodeapi.internal.network.NetworkBootstrapper
|
import net.corda.nodeapi.internal.network.NetworkBootstrapper
|
||||||
|
import picocli.CommandLine
|
||||||
import picocli.CommandLine.Option
|
import picocli.CommandLine.Option
|
||||||
|
import java.io.IOException
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
|
import java.security.KeyStoreException
|
||||||
|
import java.security.PublicKey
|
||||||
|
|
||||||
fun main(args: Array<String>) {
|
fun main(args: Array<String>) {
|
||||||
NetworkBootstrapperRunner().start(args)
|
NetworkBootstrapperRunner().start(args)
|
||||||
@ -20,16 +26,91 @@ class NetworkBootstrapperRunner : CordaCliWrapper("bootstrapper", "Bootstrap a l
|
|||||||
"It may also contain existing node directories."
|
"It may also contain existing node directories."
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
private var dir: Path = Paths.get(".")
|
var dir: Path = Paths.get(".")
|
||||||
|
|
||||||
@Option(names = ["--no-copy"], description = ["""Don't copy the CorDapp JARs into the nodes' "cordapps" directories."""])
|
@Option(names = ["--no-copy"], description = ["""Don't copy the CorDapp JARs into the nodes' "cordapps" directories."""])
|
||||||
private var noCopy: Boolean = false
|
var noCopy: Boolean = false
|
||||||
|
|
||||||
@Option(names = ["--minimum-platform-version"], description = ["The minimumPlatformVersion to use in the network-parameters."])
|
@Option(names = ["--minimum-platform-version"], description = ["The minimumPlatformVersion to use in the network-parameters."])
|
||||||
private var minimumPlatformVersion = PLATFORM_VERSION
|
var minimumPlatformVersion = PLATFORM_VERSION
|
||||||
|
|
||||||
|
@Option(names = ["--register-package-owner"],
|
||||||
|
converter = [PackageOwnerConverter::class],
|
||||||
|
description = [
|
||||||
|
"Register owner of Java package namespace in the network-parameters.",
|
||||||
|
"Format: [java-package-namespace;keystore-file;password;alias]",
|
||||||
|
" `java-package-namespace` is case insensitive and cannot be a sub-package of an existing registered namespace",
|
||||||
|
" `keystore-file` refers to the location of key store file containing the signed certificate as generated by the Java 'keytool' tool (see https://docs.oracle.com/javase/8/docs/technotes/tools/windows/keytool.html)",
|
||||||
|
" `password` to open the key store",
|
||||||
|
" `alias` refers to the name associated with a certificate containing the public key to be associated with the package namespace"
|
||||||
|
])
|
||||||
|
var registerPackageOwnership: List<PackageOwner> = mutableListOf()
|
||||||
|
|
||||||
|
@Option(names = ["--unregister-package-owner"],
|
||||||
|
converter = [JavaPackageNameConverter::class],
|
||||||
|
description = [
|
||||||
|
"Unregister owner of Java package namespace in the network-parameters.",
|
||||||
|
"Format: [java-package-namespace]",
|
||||||
|
" `java-package-namespace` is case insensitive and cannot be a sub-package of an existing registered namespace"
|
||||||
|
])
|
||||||
|
var unregisterPackageOwnership: List<JavaPackageName> = mutableListOf()
|
||||||
|
|
||||||
override fun runProgram(): Int {
|
override fun runProgram(): Int {
|
||||||
NetworkBootstrapper().bootstrap(dir.toAbsolutePath().normalize(), copyCordapps = !noCopy, minimumPlatformVersion = minimumPlatformVersion)
|
NetworkBootstrapper().bootstrap(dir.toAbsolutePath().normalize(),
|
||||||
|
copyCordapps = !noCopy,
|
||||||
|
minimumPlatformVersion = minimumPlatformVersion,
|
||||||
|
packageOwnership = registerPackageOwnership.map { Pair(it.javaPackageName, it.publicKey) }.toMap()
|
||||||
|
.plus(unregisterPackageOwnership.map { Pair(it, null) })
|
||||||
|
)
|
||||||
return 0 //exit code
|
return 0 //exit code
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
data class PackageOwner(val javaPackageName: JavaPackageName, val publicKey: PublicKey)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converter from String to PackageOwner (JavaPackageName and PublicKey)
|
||||||
|
*/
|
||||||
|
class PackageOwnerConverter : CommandLine.ITypeConverter<PackageOwner> {
|
||||||
|
override fun convert(packageOwner: String): PackageOwner {
|
||||||
|
if (!packageOwner.isBlank()) {
|
||||||
|
val packageOwnerSpec = packageOwner.split(";")
|
||||||
|
if (packageOwnerSpec.size < 4)
|
||||||
|
throw IllegalArgumentException("Package owner must specify 4 elements separated by semi-colon: 'java-package-namespace;keyStorePath;keyStorePassword;alias'")
|
||||||
|
// java package name validation
|
||||||
|
val javaPackageName = JavaPackageName(packageOwnerSpec[0])
|
||||||
|
// cater for passwords that include the argument delimiter field
|
||||||
|
val keyStorePassword =
|
||||||
|
if (packageOwnerSpec.size > 4)
|
||||||
|
packageOwnerSpec.subList(2, packageOwnerSpec.size-1).joinToString(";")
|
||||||
|
else packageOwnerSpec[2]
|
||||||
|
try {
|
||||||
|
val ks = loadKeyStore(Paths.get(packageOwnerSpec[1]), keyStorePassword)
|
||||||
|
try {
|
||||||
|
val publicKey = ks.getCertificate(packageOwnerSpec[packageOwnerSpec.size-1]).publicKey
|
||||||
|
return PackageOwner(javaPackageName,publicKey)
|
||||||
|
}
|
||||||
|
catch(kse: KeyStoreException) {
|
||||||
|
throw IllegalArgumentException("Keystore has not been initialized for alias ${packageOwnerSpec[3]}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(kse: KeyStoreException) {
|
||||||
|
throw IllegalArgumentException("Password is incorrect or the key store is damaged for keyStoreFilePath: ${packageOwnerSpec[1]} and keyStorePassword: $keyStorePassword")
|
||||||
|
}
|
||||||
|
catch(e: IOException) {
|
||||||
|
throw IllegalArgumentException("Error reading the key store from the file for keyStoreFilePath: ${packageOwnerSpec[1]} and keyStorePassword: $keyStorePassword")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else throw IllegalArgumentException("Must specify package owner argument: 'java-package-namespace;keyStorePath;keyStorePassword;alias'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converter from String to JavaPackageName.
|
||||||
|
*/
|
||||||
|
class JavaPackageNameConverter : CommandLine.ITypeConverter<JavaPackageName> {
|
||||||
|
override fun convert(packageName: String): JavaPackageName {
|
||||||
|
return JavaPackageName(packageName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -0,0 +1,167 @@
|
|||||||
|
package net.corda.bootstrapper
|
||||||
|
|
||||||
|
import net.corda.core.internal.deleteRecursively
|
||||||
|
import net.corda.core.internal.div
|
||||||
|
import net.corda.core.node.JavaPackageName
|
||||||
|
import net.corda.testing.core.ALICE_NAME
|
||||||
|
import net.corda.testing.core.BOB_NAME
|
||||||
|
import net.corda.testing.core.CHARLIE_NAME
|
||||||
|
import net.corda.testing.core.JarSignatureTestUtils.generateKey
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.junit.AfterClass
|
||||||
|
import org.junit.BeforeClass
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.rules.ExpectedException
|
||||||
|
import picocli.CommandLine
|
||||||
|
import java.nio.file.Files
|
||||||
|
|
||||||
|
class PackageOwnerParsingTest {
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
@JvmField
|
||||||
|
val expectedEx: ExpectedException = ExpectedException.none()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val ALICE = "alice"
|
||||||
|
private const val ALICE_PASS = "alicepass"
|
||||||
|
private const val BOB = "bob"
|
||||||
|
private const val BOB_PASS = "bobpass"
|
||||||
|
private const val CHARLIE = "charlie"
|
||||||
|
private const val CHARLIE_PASS = "charliepass"
|
||||||
|
|
||||||
|
private val dirAlice = Files.createTempDirectory(ALICE)
|
||||||
|
private val dirBob = Files.createTempDirectory(BOB)
|
||||||
|
private val dirCharlie = Files.createTempDirectory(CHARLIE)
|
||||||
|
|
||||||
|
val networkBootstrapper = NetworkBootstrapperRunner()
|
||||||
|
val commandLine = CommandLine(networkBootstrapper)
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
@JvmStatic
|
||||||
|
fun beforeClass() {
|
||||||
|
dirAlice.generateKey(ALICE, ALICE_PASS, ALICE_NAME.toString())
|
||||||
|
dirBob.generateKey(BOB, BOB_PASS, BOB_NAME.toString(), "EC")
|
||||||
|
dirCharlie.generateKey(CHARLIE, CHARLIE_PASS, CHARLIE_NAME.toString(), "DSA")
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterClass
|
||||||
|
@JvmStatic
|
||||||
|
fun afterClass() {
|
||||||
|
dirAlice.deleteRecursively()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `parse registration request with single mapping`() {
|
||||||
|
val aliceKeyStorePath = dirAlice / "_teststore"
|
||||||
|
val args = arrayOf("--register-package-owner", "com.example.stuff;$aliceKeyStorePath;$ALICE_PASS;$ALICE")
|
||||||
|
commandLine.parse(*args)
|
||||||
|
assertThat(networkBootstrapper.registerPackageOwnership[0].javaPackageName).isEqualTo(JavaPackageName("com.example.stuff"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `parse registration request with invalid arguments`() {
|
||||||
|
val args = arrayOf("--register-package-owner", "com.!example.stuff")
|
||||||
|
expectedEx.expect(CommandLine.ParameterException::class.java)
|
||||||
|
expectedEx.expectMessage("Package owner must specify 4 elements separated by semi-colon")
|
||||||
|
commandLine.parse(*args)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `parse registration request with incorrect keystore specification`() {
|
||||||
|
val aliceKeyStorePath = dirAlice / "_teststore"
|
||||||
|
val args = arrayOf("--register-package-owner", "com.example.stuff;$aliceKeyStorePath$ALICE_PASS")
|
||||||
|
expectedEx.expect(CommandLine.ParameterException::class.java)
|
||||||
|
expectedEx.expectMessage("Package owner must specify 4 elements separated by semi-colon")
|
||||||
|
commandLine.parse(*args)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `parse registration request with invalid java package name`() {
|
||||||
|
val args = arrayOf("--register-package-owner", "com.!example.stuff;A;B;C")
|
||||||
|
expectedEx.expect(CommandLine.ParameterException::class.java)
|
||||||
|
expectedEx.expectMessage("Invalid Java package name")
|
||||||
|
commandLine.parse(*args)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `parse registration request with invalid keystore file`() {
|
||||||
|
val args = arrayOf("--register-package-owner", "com.example.stuff;NONSENSE;B;C")
|
||||||
|
expectedEx.expect(CommandLine.ParameterException::class.java)
|
||||||
|
expectedEx.expectMessage("Error reading the key store from the file")
|
||||||
|
commandLine.parse(*args)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `parse registration request with invalid keystore password`() {
|
||||||
|
val aliceKeyStorePath = dirAlice / "_teststore"
|
||||||
|
val args = arrayOf("--register-package-owner", "com.example.stuff;$aliceKeyStorePath;BAD_PASSWORD;$ALICE")
|
||||||
|
expectedEx.expect(CommandLine.ParameterException::class.java)
|
||||||
|
expectedEx.expectMessage("Error reading the key store from the file")
|
||||||
|
commandLine.parse(*args)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `parse registration request with invalid keystore alias`() {
|
||||||
|
val aliceKeyStorePath = dirAlice / "_teststore"
|
||||||
|
val args = arrayOf("--register-package-owner", "com.example.stuff;$aliceKeyStorePath;$ALICE_PASS;BAD_ALIAS")
|
||||||
|
expectedEx.expect(CommandLine.ParameterException::class.java)
|
||||||
|
expectedEx.expectMessage("must not be null")
|
||||||
|
commandLine.parse(*args)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `parse registration request with multiple arguments`() {
|
||||||
|
val aliceKeyStorePath = dirAlice / "_teststore"
|
||||||
|
val bobKeyStorePath = dirBob / "_teststore"
|
||||||
|
val charlieKeyStorePath = dirCharlie / "_teststore"
|
||||||
|
val args = arrayOf("--register-package-owner", "com.example.stuff;$aliceKeyStorePath;$ALICE_PASS;$ALICE",
|
||||||
|
"--register-package-owner", "com.example.more.stuff;$bobKeyStorePath;$BOB_PASS;$BOB",
|
||||||
|
"--register-package-owner", "com.example.even.more.stuff;$charlieKeyStorePath;$CHARLIE_PASS;$CHARLIE")
|
||||||
|
commandLine.parse(*args)
|
||||||
|
assertThat(networkBootstrapper.registerPackageOwnership).hasSize(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `parse registration request with delimiter inclusive passwords`() {
|
||||||
|
val aliceKeyStorePath1 = dirAlice / "_alicestore1"
|
||||||
|
dirAlice.generateKey("${ALICE}1", "passw;rd", ALICE_NAME.toString(), storeName = "_alicestore1")
|
||||||
|
val aliceKeyStorePath2 = dirAlice / "_alicestore2"
|
||||||
|
dirAlice.generateKey("${ALICE}2", "\"passw;rd\"", ALICE_NAME.toString(), storeName = "_alicestore2")
|
||||||
|
val aliceKeyStorePath3 = dirAlice / "_alicestore3"
|
||||||
|
dirAlice.generateKey("${ALICE}3", "passw;rd", ALICE_NAME.toString(), storeName = "_alicestore3")
|
||||||
|
val aliceKeyStorePath4 = dirAlice / "_alicestore4"
|
||||||
|
dirAlice.generateKey("${ALICE}4", "\'passw;rd\'", ALICE_NAME.toString(), storeName = "_alicestore4")
|
||||||
|
val aliceKeyStorePath5 = dirAlice / "_alicestore5"
|
||||||
|
dirAlice.generateKey("${ALICE}5", "\"\"passw;rd\"\"", ALICE_NAME.toString(), storeName = "_alicestore5")
|
||||||
|
val packageOwnerSpecs = listOf("net.something0;$aliceKeyStorePath1;passw;rd;${ALICE}1",
|
||||||
|
"net.something1;$aliceKeyStorePath2;\"passw;rd\";${ALICE}2",
|
||||||
|
"\"net.something2;$aliceKeyStorePath3;passw;rd;${ALICE}3\"",
|
||||||
|
"net.something3;$aliceKeyStorePath4;\'passw;rd\';${ALICE}4",
|
||||||
|
"net.something4;$aliceKeyStorePath5;\"\"passw;rd\"\";${ALICE}5")
|
||||||
|
packageOwnerSpecs.forEachIndexed { i, packageOwnerSpec ->
|
||||||
|
commandLine.parse(*arrayOf("--register-package-owner", packageOwnerSpec))
|
||||||
|
assertThat(networkBootstrapper.registerPackageOwnership[0].javaPackageName).isEqualTo(JavaPackageName("net.something$i"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `parse unregister request with single mapping`() {
|
||||||
|
val args = arrayOf("--unregister-package-owner", "com.example.stuff")
|
||||||
|
commandLine.parse(*args)
|
||||||
|
assertThat(networkBootstrapper.unregisterPackageOwnership).contains(JavaPackageName("com.example.stuff"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `parse mixed register and unregister request`() {
|
||||||
|
val aliceKeyStorePath = dirAlice / "_teststore"
|
||||||
|
val args = arrayOf("--register-package-owner", "com.example.stuff;$aliceKeyStorePath;$ALICE_PASS;$ALICE",
|
||||||
|
"--unregister-package-owner", "com.example.stuff2")
|
||||||
|
commandLine.parse(*args)
|
||||||
|
assertThat(networkBootstrapper.registerPackageOwnership.map { it.javaPackageName }).contains(JavaPackageName("com.example.stuff"))
|
||||||
|
assertThat(networkBootstrapper.unregisterPackageOwnership).contains(JavaPackageName("com.example.stuff2"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Reference in New Issue
Block a user