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:
josecoll
2018-11-06 09:28:55 +00:00
committed by GitHub
parent e52f4bc2a7
commit 015a36dad6
12 changed files with 463 additions and 49 deletions

View File

@ -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.

View File

@ -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]

View File

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

View File

@ -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.

View File

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

View File

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

View File

@ -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,

View File

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

View File

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

View File

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

View File

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

View File

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