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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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.KeepForDJVM
import net.corda.core.crypto.toStringShort
import net.corda.core.identity.Party
import net.corda.core.node.services.AttachmentId
import net.corda.core.serialization.CordaSerializable
@ -140,7 +141,7 @@ data class NetworkParameters(
modifiedTime=$modifiedTime
epoch=$epoch,
packageOwnership= {
${packageOwnership.keys.joinToString()}}
${packageOwnership.entries.joinToString("\n ") { "$it.key -> ${it.value.toStringShort()}" }}
}
}"""
}
@ -172,7 +173,7 @@ class ZoneVersionTooLowException(message: String) : CordaRuntimeException(messag
@CordaSerializable
data class JavaPackageName(val name: String) {
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.
* 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.

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. */
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:
* * If you're in a companion object, use [contextLogger]

View File

@ -7,6 +7,9 @@ release, see :doc:`upgrade-notes`.
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.
* 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.
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
--------------------
@ -256,7 +304,10 @@ The network bootstrapper can be started with the following command-line options:
.. code-block:: shell
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.
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.
* ``--version``, ``-V``: Print version information and exit.
* ``--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
^^^^^^^^^^^^
``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.ConfigFactory
import net.corda.core.crypto.toStringShort
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.internal.*
import net.corda.core.internal.concurrent.fork
import net.corda.core.internal.concurrent.transpose
import net.corda.core.node.JavaPackageName
import net.corda.core.node.NetworkParameters
import net.corda.core.node.NodeInfo
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._contextSerializationEnv
import net.corda.core.utilities.days
import net.corda.core.utilities.filterNotNullValues
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.seconds
import net.corda.nodeapi.internal.*
@ -30,6 +33,7 @@ import net.corda.serialization.internal.amqp.amqpMagic
import java.io.InputStream
import java.nio.file.Path
import java.nio.file.StandardCopyOption.REPLACE_EXISTING
import java.security.PublicKey
import java.time.Instant
import java.util.*
import java.util.concurrent.Executors
@ -168,14 +172,14 @@ internal constructor(private val initSerEnv: Boolean,
}
/** 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" }
// Don't accidently include the bootstrapper jar as a CorDapp!
val bootstrapperJar = javaClass.location.toPath()
val cordappJars = directory.list { paths ->
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(
@ -183,7 +187,8 @@ internal constructor(private val initSerEnv: Boolean,
cordappJars: List<Path>,
copyCordapps: Boolean,
fromCordform: Boolean,
minimumPlatformVersion: Int = PLATFORM_VERSION
minimumPlatformVersion: Int = PLATFORM_VERSION,
packageOwnership : Map<JavaPackageName, PublicKey?> = emptyMap()
) {
directory.createDirectories()
println("Bootstrapping local test network in $directory")
@ -223,7 +228,7 @@ internal constructor(private val initSerEnv: Boolean,
val notaryInfos = gatherNotaryInfos(nodeInfoFiles, configs)
println("Generating contract implementations whitelist")
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) {
println("${if (existingNetParams == null) "New" else "Updated"} $newNetParams")
} else {
@ -355,17 +360,31 @@ internal constructor(private val initSerEnv: Boolean,
whitelist: Map<String, List<AttachmentId>>,
existingNetParams: NetworkParameters?,
nodeDirs: List<Path>,
minimumPlatformVersion: Int
minimumPlatformVersion: Int,
packageOwnership : Map<JavaPackageName, PublicKey?>
): NetworkParameters {
// TODO Add config for minimumPlatformVersion, maxMessageSize and maxTransactionSize
// TODO Add config for maxMessageSize and maxTransactionSize
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
} 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(
notaries = notaryInfos,
modifiedTime = Instant.now(),
whitelistedContractImplementations = whitelist,
packageOwnership = updatePackageOwnership,
epoch = existingNetParams.epoch + 1
)
}
@ -377,6 +396,7 @@ internal constructor(private val initSerEnv: Boolean,
maxMessageSize = 10485760,
maxTransactionSize = 10485760,
whitelistedContractImplementations = whitelist,
packageOwnership = packageOwnership.filterNotNullValues(),
epoch = 1,
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.identity.CordaX500Name
import net.corda.core.internal.*
import net.corda.core.node.JavaPackageName
import net.corda.core.node.NetworkParameters
import net.corda.core.node.NodeInfo
import net.corda.core.serialization.serialize
import net.corda.node.services.config.NotaryConfig
import net.corda.nodeapi.internal.DEV_ROOT_CA
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.config.parseAs
import net.corda.nodeapi.internal.config.toConfig
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.BOB_NAME
import net.corda.testing.core.DUMMY_NOTARY_NAME
import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.core.*
import net.corda.testing.internal.createNodeInfoAndSigned
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.After
import org.junit.Rule
import org.junit.Test
import org.junit.rules.ExpectedException
import org.junit.rules.TemporaryFolder
import java.nio.file.Path
import java.security.PublicKey
import kotlin.streams.toList
class NetworkBootstrapperTest {
@ -35,6 +34,10 @@ class NetworkBootstrapperTest {
@JvmField
val tempFolder = TemporaryFolder()
@Rule
@JvmField
val expectedEx: ExpectedException = ExpectedException.none()
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule()
@ -208,6 +211,80 @@ class NetworkBootstrapperTest {
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 fun fakeFileBytes(writeToFile: Path? = null): ByteArray {
@ -216,9 +293,9 @@ class NetworkBootstrapperTest {
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 }
bootstrapper.bootstrap(rootDir, copyCordapps, PLATFORM_VERSION)
bootstrapper.bootstrap(rootDir, copyCordapps, PLATFORM_VERSION, packageOwnership)
}
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)
}

View File

@ -107,7 +107,7 @@ class NetworkParametersTest {
JavaPackageName("com.!example.stuff") to key2
)
)
}.withMessageContaining("Attempting to whitelist illegal java package")
}.withMessageContaining("Invalid Java package name")
assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy {
NetworkParameters(1,

View File

@ -6,9 +6,6 @@ import com.google.common.jimfs.Configuration
import com.google.common.jimfs.Jimfs
import com.nhaarman.mockito_kotlin.doReturn
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.crypto.SecureHash
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.utilities.getOrThrow
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.DatabaseConfig
import net.corda.testing.common.internal.testNetworkParameters
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.rigorousMock
import net.corda.testing.internal.TestingNamedCacheFactory
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.internal.InternalMockNetwork
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.div
@ -25,8 +25,8 @@ object JarSignatureTestUtils {
.waitFor())
}
fun Path.generateKey(alias: String, password: String, name: String, keyalg: String = "RSA") =
executeProcess("keytool", "-genkey", "-keystore", "_teststore", "-storepass", "storepass", "-keyalg", keyalg, "-alias", alias, "-keypass", password, "-dname", name)
fun Path.generateKey(alias: String, storePassword: String, name: String, keyalg: String = "RSA", keyPassword: String = storePassword, storeName: String = "_teststore") =
executeProcess("keytool", "-genkeypair", "-keystore" ,storeName, "-storepass", storePassword, "-keyalg", keyalg, "-alias", alias, "-keypass", keyPassword, "-dname", name)
fun Path.createJar(fileName: String, vararg contents: String) =
executeProcess(*(arrayOf("jar", "cvf", fileName) + contents))
@ -34,9 +34,9 @@ object JarSignatureTestUtils {
fun Path.updateJar(fileName: String, vararg contents: String) =
executeProcess(*(arrayOf("jar", "uvf", fileName) + contents))
fun Path.signJar(fileName: String, alias: String, password: String): PublicKey {
executeProcess("jarsigner", "-keystore", "_teststore", "-storepass", "storepass", "-keypass", password, fileName, alias)
val ks = loadKeyStore(this.resolve("_teststore"), "storepass")
fun Path.signJar(fileName: String, alias: String, storePassword: String, keyPassword: String = storePassword): PublicKey {
executeProcess("jarsigner", "-keystore", "_teststore", "-storepass", storePassword, "-keypass", keyPassword, fileName, alias)
val ks = loadKeyStore(this.resolve("_teststore"), storePassword)
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.core.JarSignatureTestUtils.generateKey
import net.corda.core.JarSignatureTestUtils.getJarSigners
import net.corda.core.JarSignatureTestUtils.signJar
import net.corda.core.JarSignatureTestUtils.updateJar
import net.corda.testing.core.JarSignatureTestUtils.createJar
import net.corda.testing.core.JarSignatureTestUtils.generateKey
import net.corda.testing.core.JarSignatureTestUtils.getJarSigners
import net.corda.testing.core.JarSignatureTestUtils.signJar
import net.corda.testing.core.JarSignatureTestUtils.updateJar
import net.corda.core.identity.Party
import net.corda.core.internal.*
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.CHARLIE_NAME
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.AfterClass
@ -34,8 +34,8 @@ class JarSignatureCollectorTest {
@BeforeClass
@JvmStatic
fun beforeClass() {
dir.generateKey(ALICE, ALICE_PASS, ALICE_NAME.toString())
dir.generateKey(BOB, BOB_PASS, BOB_NAME.toString())
dir.generateKey(ALICE, "storepass", ALICE_NAME.toString(), keyPassword = ALICE_PASS)
dir.generateKey(BOB, "storepass", BOB_NAME.toString(), keyPassword = BOB_PASS)
(dir / "_signable1").writeLines(listOf("signable1"))
(dir / "_signable2").writeLines(listOf("signable2"))
@ -134,12 +134,12 @@ class JarSignatureCollectorTest {
// and our JarSignatureCollector
@Test
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")
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.
}
private fun signAsAlice() = dir.signJar(FILENAME, ALICE, ALICE_PASS)
private fun signAsBob() = dir.signJar(FILENAME, BOB, BOB_PASS)
private fun signAsAlice() = dir.signJar(FILENAME, ALICE, "storepass", ALICE_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.start
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 picocli.CommandLine
import picocli.CommandLine.Option
import java.io.IOException
import java.nio.file.Path
import java.nio.file.Paths
import java.security.KeyStoreException
import java.security.PublicKey
fun main(args: Array<String>) {
NetworkBootstrapperRunner().start(args)
@ -20,16 +26,91 @@ class NetworkBootstrapperRunner : CordaCliWrapper("bootstrapper", "Bootstrap a l
"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."""])
private var noCopy: Boolean = false
var noCopy: Boolean = false
@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 {
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
}
}
}
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"))
}
}