diff --git a/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt b/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt index f707a8e308..aec3891862 100644 --- a/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt +++ b/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt @@ -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. diff --git a/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt b/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt index 912fef307a..8e4a2b8f2a 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt @@ -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 Map.filterNotNullValues() = filterValues { it != null } as Map + /** * Usually you won't need this method: * * If you're in a companion object, use [contextLogger] diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 2226761cfa..f220496eb1 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -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 diff --git a/docs/source/network-bootstrapper.rst b/docs/source/network-bootstrapper.rst index d8568e7ca9..a8bc5b0c97 100644 --- a/docs/source/network-bootstrapper.rst +++ b/docs/source/network-bootstrapper.rst @@ -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`. + +A Java package namespace is case insensitive and cannot be a sub-package of an existing registered namespace. +See `Naming a Package `_ and `Naming Conventions `_ for guidelines on naming conventions. + +Registration of a java package namespace requires creation of a signed certificate as generated by the +`Java keytool `_. + +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 `_ 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 `_. + +.. 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=] [--logging-level=] - [--minimum-platform-version=] [COMMAND] + [--minimum-platform-version=] + [--register-package-owner java-package-namespace=keystore-file:password:alias] + [--unregister-package-owner java-package-namespace] + [COMMAND] * ``--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. \ No newline at end of file +``install-shell-extensions``: Install ``bootstrapper`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info. + diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt index f9ed9e0f88..db539a2c4d 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt @@ -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 = 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, copyCordapps: Boolean, fromCordform: Boolean, - minimumPlatformVersion: Int = PLATFORM_VERSION + minimumPlatformVersion: Int = PLATFORM_VERSION, + packageOwnership : Map = 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>, existingNetParams: NetworkParameters?, nodeDirs: List, - minimumPlatformVersion: Int + minimumPlatformVersion: Int, + packageOwnership : Map ): 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 ) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapperTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapperTest.kt index a3ddb3cc93..0c1b439d99 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapperTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapperTest.kt @@ -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 = 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) { + val networkParams = (rootDir / nodeDirName).networkParameters + assertThat(networkParams.packageOwnership).isEqualTo(packageOwners) + } + data class FakeNodeConfig(val myLegalName: CordaX500Name, val notary: NotaryConfig? = null) } diff --git a/node/src/test/kotlin/net/corda/node/internal/NetworkParametersTest.kt b/node/src/test/kotlin/net/corda/node/internal/NetworkParametersTest.kt index e5a352d6e7..3544ee5e85 100644 --- a/node/src/test/kotlin/net/corda/node/internal/NetworkParametersTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/NetworkParametersTest.kt @@ -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, diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt index bd584796ba..e07bc97507 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt @@ -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 diff --git a/core/src/test/kotlin/net/corda/core/JarSignatureTestUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/core/JarSignatureTestUtils.kt similarity index 70% rename from core/src/test/kotlin/net/corda/core/JarSignatureTestUtils.kt rename to testing/test-utils/src/main/kotlin/net/corda/testing/core/JarSignatureTestUtils.kt index 273d5b2234..903d05fca7 100644 --- a/core/src/test/kotlin/net/corda/core/JarSignatureTestUtils.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/core/JarSignatureTestUtils.kt @@ -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 } diff --git a/core/src/test/kotlin/net/corda/core/internal/JarSignatureCollectorTest.kt b/testing/test-utils/src/test/kotlin/net/corda/testing/core/JarSignatureCollectorTest.kt similarity index 84% rename from core/src/test/kotlin/net/corda/core/internal/JarSignatureCollectorTest.kt rename to testing/test-utils/src/test/kotlin/net/corda/testing/core/JarSignatureCollectorTest.kt index 4c124e81b7..3f356d5e7f 100644 --- a/core/src/test/kotlin/net/corda/core/internal/JarSignatureCollectorTest.kt +++ b/testing/test-utils/src/test/kotlin/net/corda/testing/core/JarSignatureCollectorTest.kt @@ -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) } diff --git a/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt b/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt index 3436bdfaf3..ed21e8cb26 100644 --- a/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt +++ b/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt @@ -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) { 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 = 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 = 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 } -} \ No newline at end of file +} + + +data class PackageOwner(val javaPackageName: JavaPackageName, val publicKey: PublicKey) + +/** + * Converter from String to PackageOwner (JavaPackageName and PublicKey) + */ +class PackageOwnerConverter : CommandLine.ITypeConverter { + 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 { + override fun convert(packageName: String): JavaPackageName { + return JavaPackageName(packageName) + } +} diff --git a/tools/bootstrapper/src/test/kotlin/net/corda/bootstrapper/PackageOwnerParsingTest.kt b/tools/bootstrapper/src/test/kotlin/net/corda/bootstrapper/PackageOwnerParsingTest.kt new file mode 100644 index 0000000000..3caf0285d3 --- /dev/null +++ b/tools/bootstrapper/src/test/kotlin/net/corda/bootstrapper/PackageOwnerParsingTest.kt @@ -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")) + } +} +