diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 6891e6e828..708384f2c6 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -52,6 +52,12 @@ + + + + + + @@ -98,6 +104,8 @@ + + diff --git a/docs/source/running-doorman.rst b/docs/source/running-doorman.rst index 451fb1f033..0b920b83d6 100644 --- a/docs/source/running-doorman.rst +++ b/docs/source/running-doorman.rst @@ -39,6 +39,8 @@ Allowed parameters are: :approveInterval: How often to process Jira approved requests in seconds. + :crlEndpoint: URL to the CRL issued by the Doorman CA. This parameter is only useful when Doorman is executing in the local signing mode. + :jira: The Jira configuration for certificate signing requests :address: The URL to use to connect to Jira @@ -51,7 +53,7 @@ Allowed parameters are: :revocation: Revocation service specific configuration - :localSigning: Configuration for local CRL signing using the file key store. If not defined t + :localSigning: Configuration for local CRL signing using the file key store. If not defined then an external signing process is assumed. :crlUpdateInterval: Validity time of the issued certificate revocation lists (in milliseconds). @@ -66,6 +68,12 @@ Allowed parameters are: :approveAll: Whether to approve all requests (defaults to false), this is for debug only. + :caCrlPath: Path (including the file name) to the location of the file containing the bytes of the CRL issued by the ROOT CA. + Note: Byte encoding is the one given by the package java.security.cert.X509CRL.encoded method - i.e. ASN.1 DER + + :emptyCrlPath: Path (including the file name) to the location of the generated file containing the bytes of the empty CRL issued by the ROOT CA. + Note: Byte encoding is the one given by the package java.security.cert.X509CRL.encoded method - i.e. ASN.1 DER + :jira: The Jira configuration for certificate revocation requests :address: The URL to use to connect to Jira @@ -88,6 +96,22 @@ Allowed parameters are: :rootStorePath: Path for the root keystore +:caCrlPath: Path (including the file name) to the location of the generated file containing the bytes of the CRL issued by the ROOT CA. + This configuration parameter is used in the ROOT_KEYGEN mode. + Note: Byte encoding is the one given by the package java.security.cert.X509CRL.encoded method - i.e. ASN.1 DER + +:caCrlUrl: URL to the CRL issued by the ROOT CA. This URL is going to be included in the generated CRL that is signed by the ROOT CA. + This configuration parameter is used in the ROOT_KEYGEN and CA_KEYGEN modes. + +:emptyCrlPath: Path (including the file name) to the location of the generated file containing the bytes of the empty CRL issued by the ROOT CA. + This configuration parameter is used in the ROOT_KEYGEN mode. + Note: Byte encoding is the one given by the package java.security.cert.X509CRL.encoded method - i.e. ASN.1 DER + This CRL is to allow nodes to operate in the strict CRL checking mode. This mode requires all the certificates in the chain being validated + to point a CRL. Since the TLS-level certificate is managed by the nodes, this CRL is a facility one for infrastructures without CRL provisioning. + +:emptyCrlUrl: URL to the empty CRL issued by the ROOT CA. This URL is going to be included in the generated empty CRL that is signed by the ROOT CA. + This configuration parameter is used in the ROOT_KEYGEN mode. + Bootstrapping the network parameters ------------------------------------ When doorman is running it will serve the current network parameters. The first time doorman is diff --git a/launcher/build.gradle b/launcher/build.gradle new file mode 100644 index 0000000000..1c65f08593 --- /dev/null +++ b/launcher/build.gradle @@ -0,0 +1,19 @@ +group 'com.r3.corda' +version 'R3.CORDA-3.0-SNAPSHOT' + +apply plugin: 'java' +apply plugin: 'kotlin' + +dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" + compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" +} + +ext { + loaderClassName = "net.corda.launcher.Loader" + launcherClassName = "net.corda.launcher.Launcher" +} + +jar { + baseName 'corda-launcher' +} diff --git a/launcher/src/main/kotlin/net/corda/launcher/Launcher.kt b/launcher/src/main/kotlin/net/corda/launcher/Launcher.kt new file mode 100644 index 0000000000..c9f9bffec1 --- /dev/null +++ b/launcher/src/main/kotlin/net/corda/launcher/Launcher.kt @@ -0,0 +1,81 @@ +@file:JvmName("Launcher") + +package net.corda.launcher + +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import kotlin.system.exitProcess + +fun main(args: Array) { + + if(args.isEmpty()) { + println("Usage: launcher [args]") + exitProcess(0) + } + + // TODO: --base-directory is specific of the Node app, it should be controllable by a config property + val nodeBaseDir = Paths.get(Settings.WORKING_DIR) + .resolve(getBaseDirectory(args) ?: ".") + .toAbsolutePath() + + val appClassLoader = setupClassLoader(nodeBaseDir) + + val appMain = try { + appClassLoader + .loadClass(args[0]) + .getMethod("main", Array::class.java) + } catch (e: Exception) { + System.err.println("Error looking for method 'main' in class ${args[0]}:") + e.printStackTrace() + exitProcess(1) + } + + // Propagate current working directory via system property, to patch it after javapackager + System.setProperty("corda-launcher.cwd", Settings.WORKING_DIR) + System.setProperty("user.dir", Settings.WORKING_DIR) + + try { + appMain.invoke(null, args.sliceArray(1..args.lastIndex)) + } catch (e: Exception) { + e.printStackTrace() + exitProcess(1) + } +} + +private fun setupClassLoader(nodeBaseDir: Path): ClassLoader { + val sysClassLoader = ClassLoader.getSystemClassLoader() + + val appClassLoader = (sysClassLoader as? Loader) ?: { + println("WARNING: failed to override system classloader") + Loader(sysClassLoader) + } () + + // Lookup plugins and extend classpath + val pluginURLs = Settings.PLUGINS.flatMap { + val entry = nodeBaseDir.resolve(it) + if (Files.isDirectory(entry)) { + entry.jarFiles() + } else { + setOf(entry) + } + }.map { it.toUri().toURL() } + + appClassLoader.augmentClasspath(pluginURLs) + + // For logging + System.setProperty("corda-launcher.appclassloader.urls", appClassLoader.urLs.joinToString(":")) + + return appClassLoader +} + +private fun getBaseDirectory(args: Array): String? { + val idx = args.indexOf("--base-directory") + return if (idx != -1 && idx < args.lastIndex) { + args[idx + 1] + } else null +} + +private fun Path.jarFiles(): List { + return Files.newDirectoryStream(this).filter { it.toString().endsWith(".jar") } +} diff --git a/launcher/src/main/kotlin/net/corda/launcher/Loader.kt b/launcher/src/main/kotlin/net/corda/launcher/Loader.kt new file mode 100644 index 0000000000..49e35d3531 --- /dev/null +++ b/launcher/src/main/kotlin/net/corda/launcher/Loader.kt @@ -0,0 +1,11 @@ +package net.corda.launcher + +import java.net.URL +import java.net.URLClassLoader + +class Loader(parent: ClassLoader?): URLClassLoader(Settings.CLASSPATH.toTypedArray(), parent) { + + fun augmentClasspath(urls: List) { + urls.forEach { addURL(it) } + } +} diff --git a/launcher/src/main/kotlin/net/corda/launcher/Settings.kt b/launcher/src/main/kotlin/net/corda/launcher/Settings.kt new file mode 100644 index 0000000000..74bc91f79b --- /dev/null +++ b/launcher/src/main/kotlin/net/corda/launcher/Settings.kt @@ -0,0 +1,59 @@ +package net.corda.launcher + +import java.io.FileInputStream +import java.net.URL +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.util.* +import kotlin.collections.HashSet + +// Expose Corda bootstrapping settings from property file +object Settings { + + // JavaPackager reset cwd to the "/apps" subfolder, so its location is in the parent directory + private val LAUNCHER_PATH = Paths.get("..") + + // Launcher property file + private val CORDA_RUNTIME_SETTINGS = LAUNCHER_PATH.resolve("runtime.properties") + + // The application working directory + val WORKING_DIR: String = System.getenv("CORDA_LAUNCHER_CWD") ?: ".." + + // Application classpath + val CLASSPATH: List + + // Plugin directories (all contained jar files are added to classpath) + val PLUGINS: List + + // Path of the "lib" subdirectory in bundle + private val LIBPATH: Path + + init { + val settings = Properties().apply { + load(FileInputStream(CORDA_RUNTIME_SETTINGS.toFile())) + } + + LIBPATH = Paths.get(settings.getProperty("libpath") ?: ".") + CLASSPATH = parseClasspath(settings) + PLUGINS = parsePlugins(settings) + } + + private fun parseClasspath(config: Properties): List { + val libDir = LAUNCHER_PATH.resolve(LIBPATH).toAbsolutePath() + val cp = config.getProperty("classpath") ?: + throw Error("Missing 'classpath' property from config") + + return cp.split(':').map { + libDir.resolve(it).toUri().toURL() + } + } + + private fun parsePlugins(config: Properties): List { + val ext = config.getProperty("plugins") + + return ext?.let { + it.split(':').map { Paths.get(it) } + } ?: emptyList() + } +} diff --git a/network-management/registration-tool/src/main/kotlin/com/r3/corda/networkmanage/registration/NotaryRegistrationTool.kt b/network-management/registration-tool/src/main/kotlin/com/r3/corda/networkmanage/registration/NotaryRegistrationTool.kt index 76ed6803b1..2ff23b6295 100644 --- a/network-management/registration-tool/src/main/kotlin/com/r3/corda/networkmanage/registration/NotaryRegistrationTool.kt +++ b/network-management/registration-tool/src/main/kotlin/com/r3/corda/networkmanage/registration/NotaryRegistrationTool.kt @@ -68,4 +68,5 @@ data class NotaryRegistrationConfig(val legalName: CordaX500Name, val networkRootTrustStorePassword: String?, val trustStorePassword: String?, val keystorePath: Path?, - val crlCheckSoftFail: Boolean) + val crlCheckSoftFail: Boolean, + val crlDistributionPoint: URL? = null) diff --git a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/common/HsmBaseTest.kt b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/common/HsmBaseTest.kt index fcb5e24906..623be2f799 100644 --- a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/common/HsmBaseTest.kt +++ b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/common/HsmBaseTest.kt @@ -26,7 +26,7 @@ import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.internal.IntegrationTest import net.corda.testing.internal.IntegrationTestSchemas -import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties +import net.corda.testing.node.internal.makeTestDataSourceProperties import net.corda.testing.node.internal.makeTestDatabaseProperties import org.junit.Before import org.junit.ClassRule @@ -185,10 +185,10 @@ abstract class HsmBaseTest : IntegrationTest() { } fun makeTestDataSourceProperties(): Properties { - return makeTestDataSourceProperties(DOORMAN_DB_NAME) + return makeTestDataSourceProperties(DOORMAN_DB_NAME, configSupplier = configSupplierForSupportedDatabases()) } fun makeTestDatabaseProperties(): DatabaseConfig { - return makeTestDatabaseProperties(DOORMAN_DB_NAME) + return makeTestDatabaseProperties(DOORMAN_DB_NAME, configSupplier = configSupplierForSupportedDatabases()) } } \ No newline at end of file diff --git a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/common/TestUtils.kt b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/common/TestUtils.kt index ce84d4c3b2..b8dc16810e 100644 --- a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/common/TestUtils.kt +++ b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/common/TestUtils.kt @@ -10,16 +10,26 @@ package com.r3.corda.networkmanage.common +import com.r3.corda.networkmanage.common.utils.createSignedCrl +import com.r3.corda.networkmanage.doorman.signer.LocalSigner import com.typesafe.config.Config import com.typesafe.config.ConfigFactory import net.corda.core.crypto.SecureHash +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.days +import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair import net.corda.testing.database.DatabaseConstants +import net.corda.testing.node.internal.databaseProviderDataSourceConfig +import org.apache.commons.io.FileUtils +import org.junit.rules.TemporaryFolder +import java.net.URL +import java.nio.file.Path const val HOST = "localhost" const val DOORMAN_DB_NAME = "doorman" -fun networkMapInMemoryH2DataSourceConfig(nodeName: String? = null, postfix: String? = null) : Config { +fun networkMapInMemoryH2DataSourceConfig(nodeName: String? = null, postfix: String? = null): Config { val nodeName = nodeName ?: SecureHash.randomSHA256().toString() val h2InstanceName = if (postfix != null) nodeName + "_" + postfix else nodeName @@ -28,4 +38,26 @@ fun networkMapInMemoryH2DataSourceConfig(nodeName: String? = null, postfix: Stri DatabaseConstants.DATA_SOURCE_URL to "jdbc:h2:mem:${h2InstanceName};DB_CLOSE_DELAY=-1", DatabaseConstants.DATA_SOURCE_USER to "sa", DatabaseConstants.DATA_SOURCE_PASSWORD to "")) -} \ No newline at end of file +} + +fun generateEmptyCrls(tempFolder: TemporaryFolder, rootCertAndKeyPair: CertificateAndKeyPair, directEndpoint: URL, indirectEndpoint: URL): Pair { + val localSigner = LocalSigner(rootCertAndKeyPair) + val directCrl = createSignedCrl(rootCertAndKeyPair.certificate, directEndpoint, 10.days, localSigner, emptyList(), false) + val indirectCrl = createSignedCrl(rootCertAndKeyPair.certificate, indirectEndpoint, 10.days, localSigner, emptyList(), true) + val directCrlFile = tempFolder.newFile() + FileUtils.writeByteArrayToFile(directCrlFile, directCrl.encoded) + val indirectCrlFile = tempFolder.newFile() + FileUtils.writeByteArrayToFile(indirectCrlFile, indirectCrl.encoded) + return Pair(directCrlFile.toPath(), indirectCrlFile.toPath()) +} + +fun getCaCrlEndpoint(serverAddress: NetworkHostAndPort) = URL("http://$serverAddress/certificate-revocation-list/root") +fun getEmptyCrlEndpoint(serverAddress: NetworkHostAndPort) = URL("http://$serverAddress/certificate-revocation-list/empty") +fun getNodeCrlEndpoint(serverAddress: NetworkHostAndPort) = URL("http://$serverAddress/certificate-revocation-list/doorman") + +//TODO add more dbs to test once doorman supports them +fun configSupplierForSupportedDatabases(): (String?, String?) -> Config = + when (System.getProperty("custom.databaseProvider", "")) { + "integration-sql-server", "integration-azure-sql" -> ::databaseProviderDataSourceConfig + else -> { _, _ -> ConfigFactory.empty() } + } \ No newline at end of file diff --git a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/doorman/NetworkParametersUpdateTest.kt b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/doorman/NetworkParametersUpdateTest.kt index 993985a985..2049753f83 100644 --- a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/doorman/NetworkParametersUpdateTest.kt +++ b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/doorman/NetworkParametersUpdateTest.kt @@ -1,6 +1,7 @@ package com.r3.corda.networkmanage.doorman import com.r3.corda.networkmanage.common.DOORMAN_DB_NAME +import com.r3.corda.networkmanage.common.configSupplierForSupportedDatabases import com.r3.corda.networkmanage.common.networkMapInMemoryH2DataSourceConfig import com.r3.corda.networkmanage.common.utils.CertPathAndKey import com.r3.corda.networkmanage.doorman.signer.LocalSigner @@ -154,7 +155,8 @@ class NetworkParametersUpdateTest : IntegrationTest() { private fun startServer(startNetworkMap: Boolean = true): NetworkManagementServer { val doormanConfig = DoormanConfig(approveAll = true, jira = null, approveInterval = timeoutMillis) - val server = NetworkManagementServer(makeTestDataSourceProperties(DOORMAN_DB_NAME, dbNamePostfix, fallBackConfigSupplier = ::networkMapInMemoryH2DataSourceConfig), DatabaseConfig(runMigration = true), doormanConfig, null) + val server = NetworkManagementServer(makeTestDataSourceProperties(DOORMAN_DB_NAME, dbNamePostfix, configSupplier = configSupplierForSupportedDatabases(), fallBackConfigSupplier = ::networkMapInMemoryH2DataSourceConfig), + DatabaseConfig(runMigration = true), doormanConfig, null) server.start( serverAddress, CertPathAndKey(listOf(doormanCa.certificate, rootCaCert), doormanCa.keyPair.private), @@ -173,7 +175,7 @@ class NetworkParametersUpdateTest : IntegrationTest() { private fun applyNetworkParametersAndStart(networkParametersCmd: NetworkParametersCmd) { server?.close() NetworkManagementServer( - makeTestDataSourceProperties(DOORMAN_DB_NAME, dbNamePostfix, fallBackConfigSupplier = ::networkMapInMemoryH2DataSourceConfig), + makeTestDataSourceProperties(DOORMAN_DB_NAME, dbNamePostfix, configSupplier = configSupplierForSupportedDatabases(), fallBackConfigSupplier = ::networkMapInMemoryH2DataSourceConfig), DatabaseConfig(runMigration = true), DoormanConfig(approveAll = true, jira = null, approveInterval = timeoutMillis), null).use { diff --git a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/doorman/NodeRegistrationTest.kt b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/doorman/NodeRegistrationTest.kt index fa1be7a347..955d25871f 100644 --- a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/doorman/NodeRegistrationTest.kt +++ b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/doorman/NodeRegistrationTest.kt @@ -10,8 +10,7 @@ package com.r3.corda.networkmanage.doorman -import com.r3.corda.networkmanage.common.DOORMAN_DB_NAME -import com.r3.corda.networkmanage.common.networkMapInMemoryH2DataSourceConfig +import com.r3.corda.networkmanage.common.* import com.r3.corda.networkmanage.common.utils.CertPathAndKey import com.r3.corda.networkmanage.doorman.signer.LocalSigner import net.corda.cordform.CordformNode @@ -43,7 +42,9 @@ import net.corda.testing.node.internal.makeTestDataSourceProperties import net.corda.testing.node.internal.makeTestDatabaseProperties import org.assertj.core.api.Assertions.assertThat import org.junit.* +import org.junit.rules.TemporaryFolder import java.net.URL +import java.nio.file.Path import java.security.cert.X509Certificate import kotlin.streams.toList @@ -77,17 +78,25 @@ class NodeRegistrationTest : IntegrationTest() { private var server: NetworkManagementServer? = null + @Rule + @JvmField + val tempFolder = TemporaryFolder() + private val doormanConfig: DoormanConfig get() = DoormanConfig(approveAll = true, jira = null, approveInterval = timeoutMillis) - private val revocationConfig: CertificateRevocationConfig - get() = CertificateRevocationConfig( + private lateinit var revocationConfig: CertificateRevocationConfig + + private fun createCertificateRevocationConfig(emptyCrlPath: Path, caCrlPath: Path): CertificateRevocationConfig { + return CertificateRevocationConfig( approveAll = true, jira = null, approveInterval = timeoutMillis, crlCacheTimeout = timeoutMillis, localSigning = CertificateRevocationConfig.LocalSigning( - crlEndpoint = URL("http://test.com/crl"), - crlUpdateInterval = timeoutMillis) - ) + crlEndpoint = getNodeCrlEndpoint(serverAddress), + crlUpdateInterval = timeoutMillis), + emptyCrlPath = emptyCrlPath, + caCrlPath = caCrlPath) + } @Before fun init() { @@ -96,6 +105,8 @@ class NodeRegistrationTest : IntegrationTest() { rootCaCert = rootCa.certificate this.doormanCa = doormanCa networkMapCa = createDevNetworkMapCa(rootCa) + val (caCrlPath, emptyCrlPath) = generateEmptyCrls(tempFolder, rootCa, getCaCrlEndpoint(serverAddress), getEmptyCrlEndpoint(serverAddress)) + revocationConfig = createCertificateRevocationConfig(emptyCrlPath, caCrlPath) } @After @@ -174,7 +185,8 @@ class NodeRegistrationTest : IntegrationTest() { } private fun startServer(startNetworkMap: Boolean = true): NetworkManagementServer { - val server = NetworkManagementServer(makeTestDataSourceProperties(DOORMAN_DB_NAME, dbNamePostfix, fallBackConfigSupplier = ::networkMapInMemoryH2DataSourceConfig), makeTestDatabaseProperties(DOORMAN_DB_NAME), doormanConfig, revocationConfig) + val server = NetworkManagementServer(makeTestDataSourceProperties(DOORMAN_DB_NAME, dbNamePostfix, configSupplier = configSupplierForSupportedDatabases(), fallBackConfigSupplier = ::networkMapInMemoryH2DataSourceConfig), + makeTestDatabaseProperties(configSupplier = configSupplierForSupportedDatabases()), doormanConfig, revocationConfig) server.start( serverAddress, CertPathAndKey(listOf(doormanCa.certificate, rootCaCert), doormanCa.keyPair.private), @@ -192,7 +204,8 @@ class NodeRegistrationTest : IntegrationTest() { private fun applyNetworkParametersAndStart(networkParametersCmd: NetworkParametersCmd) { server?.close() - NetworkManagementServer(makeTestDataSourceProperties(DOORMAN_DB_NAME, dbNamePostfix, fallBackConfigSupplier = ::networkMapInMemoryH2DataSourceConfig), makeTestDatabaseProperties(DOORMAN_DB_NAME), doormanConfig, revocationConfig).use { + NetworkManagementServer(makeTestDataSourceProperties(DOORMAN_DB_NAME, dbNamePostfix, configSupplier = configSupplierForSupportedDatabases(), fallBackConfigSupplier = ::networkMapInMemoryH2DataSourceConfig), + makeTestDatabaseProperties(configSupplier = configSupplierForSupportedDatabases()), doormanConfig, revocationConfig).use { it.netParamsUpdateHandler.processNetworkParameters(networkParametersCmd) } server = startServer(startNetworkMap = true) diff --git a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/SigningServiceIntegrationTest.kt b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/SigningServiceIntegrationTest.kt index e7db87b120..db7d13bdb4 100644 --- a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/SigningServiceIntegrationTest.kt +++ b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/SigningServiceIntegrationTest.kt @@ -11,8 +11,7 @@ package com.r3.corda.networkmanage.hsm import com.nhaarman.mockito_kotlin.* -import com.r3.corda.networkmanage.common.HOST -import com.r3.corda.networkmanage.common.HsmBaseTest +import com.r3.corda.networkmanage.common.* import com.r3.corda.networkmanage.common.persistence.configureDatabase import com.r3.corda.networkmanage.doorman.CertificateRevocationConfig import com.r3.corda.networkmanage.doorman.DoormanConfig @@ -26,9 +25,6 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.internal.createDirectories import net.corda.core.internal.div import net.corda.core.internal.uncheckedCast -import net.corda.core.utilities.NetworkHostAndPort -import net.corda.core.utilities.hours -import net.corda.core.utilities.minutes import net.corda.core.utilities.seconds import net.corda.node.NodeRegistrationOption import net.corda.node.services.config.NodeConfiguration @@ -40,6 +36,7 @@ import net.corda.nodeapi.internal.crypto.X509KeyStore import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.SerializationEnvironmentRule +import net.corda.testing.driver.PortAllocation import net.corda.testing.internal.createDevIntermediateCaCertPath import net.corda.testing.internal.rigorousMock import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest @@ -48,6 +45,7 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import java.net.URL +import java.nio.file.Path import java.security.cert.X509Certificate import java.util.* import javax.persistence.PersistenceException @@ -59,25 +57,31 @@ class SigningServiceIntegrationTest : HsmBaseTest() { @JvmField val testSerialization = SerializationEnvironmentRule(true) + private val portAllocation = PortAllocation.Incremental(10000) + private val serverAddress = portAllocation.nextHostAndPort() + private lateinit var timer: Timer private lateinit var rootCaCert: X509Certificate private lateinit var intermediateCa: CertificateAndKeyPair + private val timeoutMillis = 5.seconds.toMillis() private lateinit var dbName: String private val doormanConfig: DoormanConfig get() = DoormanConfig(approveAll = true, approveInterval = 2.seconds.toMillis(), jira = null) - private val revocationConfig: CertificateRevocationConfig - get() = CertificateRevocationConfig( + private lateinit var revocationConfig: CertificateRevocationConfig + + private fun createCertificateRevocationConfig(emptyCrlPath: Path, caCrlPath: Path): CertificateRevocationConfig { + return CertificateRevocationConfig( approveAll = true, jira = null, - crlCacheTimeout = 30.minutes.toMillis(), - approveInterval = 10.minutes.toMillis(), + approveInterval = timeoutMillis, + crlCacheTimeout = timeoutMillis, localSigning = CertificateRevocationConfig.LocalSigning( - crlEndpoint = URL("http://test.com/crl"), - crlUpdateInterval = 2.hours.toMillis() - ) - ) - + crlEndpoint = getNodeCrlEndpoint(serverAddress), + crlUpdateInterval = timeoutMillis), + emptyCrlPath = emptyCrlPath, + caCrlPath = caCrlPath) + } @Before override fun setUp() { @@ -87,6 +91,8 @@ class SigningServiceIntegrationTest : HsmBaseTest() { val (rootCa, intermediateCa) = createDevIntermediateCaCertPath() rootCaCert = rootCa.certificate this.intermediateCa = intermediateCa + val (caCrlPath, emptyCrlPath) = generateEmptyCrls(tempFolder, rootCa, getCaCrlEndpoint(serverAddress), getEmptyCrlEndpoint(serverAddress)) + revocationConfig = createCertificateRevocationConfig(emptyCrlPath, caCrlPath) } @After @@ -116,7 +122,7 @@ class SigningServiceIntegrationTest : HsmBaseTest() { //Start doorman server NetworkManagementServer(makeTestDataSourceProperties(), makeTestDatabaseProperties(), doormanConfig, revocationConfig).use { server -> server.start( - hostAndPort = NetworkHostAndPort(HOST, 0), + hostAndPort = serverAddress, csrCertPathAndKey = null, startNetworkMap = null) val doormanHostAndPort = server.hostAndPort diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationListStorage.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationListStorage.kt index 16a5ee5ab3..0782d73754 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationListStorage.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationListStorage.kt @@ -26,7 +26,7 @@ class PersistentCertificateRevocationListStorage(private val database: CordaPers override fun saveCertificateRevocationList(crl: X509CRL, crlIssuer: CrlIssuer, signedBy: String, revokedAt: Instant) { database.transaction { - crl.revokedCertificates.forEach { + crl.revokedCertificates?.forEach { revokeCertificate(it.serialNumber, revokedAt, this) } session.save(CertificateRevocationListEntity( diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationRequestStorage.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationRequestStorage.kt index aef8f8d8e9..42ec4db79b 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationRequestStorage.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationRequestStorage.kt @@ -5,6 +5,7 @@ import com.r3.corda.networkmanage.common.persistence.entity.CertificateRevocatio import com.r3.corda.networkmanage.common.persistence.entity.CertificateSigningRequestEntity import net.corda.core.crypto.SecureHash import net.corda.core.identity.CordaX500Name +import net.corda.core.utilities.contextLogger import net.corda.nodeapi.internal.network.CertificateRevocationRequest import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseTransaction @@ -25,6 +26,7 @@ class PersistentCertificateRevocationRequestStorage(private val database: CordaP CRLReason.SUPERSEDED, CRLReason.UNSPECIFIED ) + val logger = contextLogger() } override fun saveRevocationRequest(request: CertificateRevocationRequest): String { @@ -60,7 +62,7 @@ class PersistentCertificateRevocationRequestStorage(private val database: CordaP } } - private fun validate(request:CertificateRevocationRequest) { + private fun validate(request: CertificateRevocationRequest) { require(request.reason in ALLOWED_REASONS) { "The given revocation reason is not allowed." } } @@ -140,11 +142,20 @@ class PersistentCertificateRevocationRequestStorage(private val database: CordaP if (revocation == null) { throw NoSuchElementException("Error while approving! Certificate revocation id=$id does not exist") } else { - session.merge(revocation.copy( - status = RequestStatus.APPROVED, - modifiedAt = Instant.now(), - modifiedBy = approvedBy - )) + when (revocation.status) { + RequestStatus.TICKET_CREATED -> { + session.merge(revocation.copy( + status = RequestStatus.APPROVED, + modifiedAt = Instant.now(), + modifiedBy = approvedBy + )) + logger.debug("`request id` = $requestId marked as APPROVED") + } + else -> { + logger.warn("`request id` = $requestId cannot be marked as APPROVED. Its current status is ${revocation.status}") + return@transaction + } + } } } } @@ -155,27 +166,45 @@ class PersistentCertificateRevocationRequestStorage(private val database: CordaP if (revocation == null) { throw NoSuchElementException("Error while rejecting! Certificate revocation id=$id does not exist") } else { - session.merge(revocation.copy( - status = RequestStatus.REJECTED, - modifiedAt = Instant.now(), - modifiedBy = rejectedBy, - remark = reason - )) + when (revocation.status) { + RequestStatus.TICKET_CREATED -> { + session.merge(revocation.copy( + status = RequestStatus.REJECTED, + modifiedAt = Instant.now(), + modifiedBy = rejectedBy, + remark = reason + )) + logger.debug("`request id` = $requestId marked as REJECTED") + } + else -> { + logger.warn("`request id` = $requestId cannot be marked as REJECTED. Its current status is ${revocation.status}") + return@transaction + } + } } } } override fun markRequestTicketCreated(requestId: String) { - // Even though, we have an assumption that there is always a single instance of the doorman service running, - // the SERIALIZABLE isolation level is used here just to ensure data consistency between the updates. - return database.transaction(TransactionIsolationLevel.SERIALIZABLE) { - val request = requireNotNull(getRevocationRequestEntity(requestId, RequestStatus.NEW)) { - "Error when creating request ticket with id: $requestId. Request does not exist or its status is not NEW." + database.transaction { + val revocation = getRevocationRequestEntity(requestId) + if (revocation == null) { + throw NoSuchElementException("Error while marking the request as ticket created! Certificate revocation id=$id does not exist") + } else { + when (revocation.status) { + RequestStatus.NEW -> { + session.merge(revocation.copy( + modifiedAt = Instant.now(), + status = RequestStatus.TICKET_CREATED + )) + logger.debug("`request id` = $requestId marked as TICKED_CREATED") + } + else -> { + logger.warn("`request id` = $requestId cannot be marked as TICKED_CREATED. Its current status is ${revocation.status}") + return@transaction + } + } } - val update = request.copy( - modifiedAt = Instant.now(), - status = RequestStatus.TICKET_CREATED) - session.merge(update) } } diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateSigningRequestStorage.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateSigningRequestStorage.kt index c8b301bd05..e15cd61822 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateSigningRequestStorage.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateSigningRequestStorage.kt @@ -177,7 +177,7 @@ class PersistentCertificateSigningRequestStorage(private val database: CordaPers existingRequestByPubKeyHash?.let { // Compare subject, attribute. // We cannot compare the request directly because it contains nonce. - if (it.request.subject == request.subject && it.request.attributes.asList() == request.attributes.asList()) { + if (certNotRevoked(it) && it.request.subject == request.subject && it.request.attributes.asList() == request.attributes.asList()) { return it.requestId } else { //TODO Consider following scenario: There is a CSR that is signed but the certificate itself has expired or was revoked @@ -190,11 +190,19 @@ class PersistentCertificateSigningRequestStorage(private val database: CordaPers // TODO consider scenario: There is a CSR that is signed but the certificate itself has expired or was revoked // Also, at the moment we assume that once the CSR is approved it cannot be rejected. // What if we approved something by mistake. - if (nonRejectedRequest(CertificateSigningRequestEntity::legalName.name, legalName) != null) throw RequestValidationException(legalName, rejectMessage = "Duplicate legal name") - + val existingRequestByLegalName = nonRejectedRequest(CertificateSigningRequestEntity::legalName.name, legalName) + existingRequestByLegalName?.let { + if (certNotRevoked(it)) { + throw RequestValidationException(legalName, rejectMessage = "Duplicate legal name") + } + } return null } + private fun certNotRevoked(request: CertificateSigningRequestEntity): Boolean { + return request.status != RequestStatus.DONE || request.certificateData?.certificateStatus != CertificateStatus.REVOKED + } + /** * Retrieve "non-rejected" request which matches provided column and value predicate. */ diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/utils/CrlUtils.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/utils/CrlUtils.kt index 1691a52e1b..bf48596fc9 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/utils/CrlUtils.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/utils/CrlUtils.kt @@ -23,14 +23,15 @@ fun createSignedCrl(issuerCertificate: X509Certificate, endpointUrl: URL, nextUpdateInterval: Duration, signer: Signer, - includeInCrl: List): X509CRL { + includeInCrl: List, + indirectIssuingPoint: Boolean = false): X509CRL { val extensionUtils = JcaX509ExtensionUtils() - val builder = X509v2CRLBuilder(X500Name.getInstance(issuerCertificate.issuerX500Principal.encoded), Date()) + val builder = X509v2CRLBuilder(X500Name.getInstance(issuerCertificate.subjectX500Principal.encoded), Date()) builder.addExtension(Extension.authorityKeyIdentifier, false, extensionUtils.createAuthorityKeyIdentifier(issuerCertificate)) val issuingDistributionPointName = GeneralName(GeneralName.uniformResourceIdentifier, endpointUrl.toString()) - val issuingDistributionPoint = IssuingDistributionPoint(DistributionPointName(GeneralNames(issuingDistributionPointName)), false, false) + val issuingDistributionPoint = IssuingDistributionPoint(DistributionPointName(GeneralNames(issuingDistributionPointName)), indirectIssuingPoint, false) builder.addExtension(Extension.issuingDistributionPoint, true, issuingDistributionPoint) - builder.setNextUpdate(Date((Instant.now() + nextUpdateInterval).toEpochMilli())) + builder.setNextUpdate(Date(Instant.now().toEpochMilli() + nextUpdateInterval.toMillis())) includeInCrl.forEach { builder.addCRLEntry(it.certificateSerialNumber, Date(it.modifiedAt.toEpochMilli()), it.reason.ordinal) } diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/CrrJiraCient.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/CrrJiraCient.kt index f6d82df265..878696f4ff 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/CrrJiraCient.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/CrrJiraCient.kt @@ -14,6 +14,7 @@ import com.atlassian.jira.rest.client.api.JiraRestClient import com.atlassian.jira.rest.client.api.domain.input.IssueInputBuilder import com.atlassian.jira.rest.client.api.domain.input.TransitionInput import com.r3.corda.networkmanage.common.persistence.CertificateRevocationRequestData +import net.corda.core.identity.CordaX500Name import net.corda.core.utilities.contextLogger class CrrJiraClient(restClient: JiraRestClient, projectCode: String) : JiraClient(restClient, projectCode) { @@ -31,20 +32,29 @@ class CrrJiraClient(restClient: JiraRestClient, projectCode: String) : JiraClien "Certificate serial number: ${revocationRequest.certificateSerialNumber}\n" + "Revocation reason: ${revocationRequest.reason.name}\n" + "Reporter: ${revocationRequest.reporter}\n" + - "CSR request ID: ${revocationRequest.certificateSigningRequestId}" + "Original CSR request ID: ${revocationRequest.certificateSigningRequestId}" + + val subject = CordaX500Name.build(revocationRequest.certificate.subjectX500Principal) + val ticketSummary = if (subject.organisationUnit != null) { + "${subject.organisationUnit}, ${subject.organisation}" + } else { + subject.organisation + } val issue = IssueInputBuilder().setIssueTypeId(taskIssueType.id) .setProjectKey(projectCode) .setDescription(ticketDescription) + .setSummary(ticketSummary) .setFieldValue(requestIdField.id, revocationRequest.requestId) // This will block until the issue is created. - val issueId = restClient.issueClient.createIssue(issue.build()).fail { logger.error("Exception when creating JIRA issue.", it) }.claim().key - val createdIssue = checkNotNull(getIssueById(issueId)) { "Missing the JIRA ticket for the request ID: $issueId" } + restClient.issueClient.createIssue(issue.build()).fail { logger.error("Exception when creating JIRA issue.", it) }.claim().key + val createdIssue = checkNotNull(getIssueById(revocationRequest.requestId)) { "Missing the JIRA ticket for the request ID: ${revocationRequest.requestId}" } restClient.issueClient.addAttachment(createdIssue.attachmentsUri, revocationRequest.certificate.encoded.inputStream(), "${revocationRequest.certificateSerialNumber}.cer") - .fail { CsrJiraClient.logger.error("Error processing request '${createdIssue.key}' : Exception when uploading attachment to JIRA.", it) }.claim() + .fail { logger.error("Error processing request '${createdIssue.key}' : Exception when uploading attachment to JIRA.", it) }.claim() } fun updateDoneCertificateRevocationRequest(requestId: String) { + logger.debug("Marking JIRA ticket with `request ID` = $requestId as DONE.") val issue = requireNotNull(getIssueById(requestId)) { "Missing the JIRA ticket for the request ID: $requestId" } restClient.issueClient.transition(issue, TransitionInput(getTransitionId(DONE_TRANSITION_KEY, issue))).fail { logger.error("Exception when transiting JIRA status.", it) }.claim() } diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/DoormanParameters.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/DoormanParameters.kt index 72e78be49e..088d9d14dc 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/DoormanParameters.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/DoormanParameters.kt @@ -11,6 +11,7 @@ package com.r3.corda.networkmanage.doorman import com.google.common.primitives.Booleans +import net.corda.core.internal.div import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.seconds import net.corda.nodeapi.internal.config.OldConfig @@ -41,7 +42,15 @@ data class NetworkManagementServerConfig( // TODO: Move local signing to signing // TODO Should be part of a localSigning sub-config val rootKeystorePassword: String?, // TODO Should be part of a localSigning sub-config - val rootPrivateKeyPassword: String? + val rootPrivateKeyPassword: String?, + // TODO Should be part of a localSigning sub-config + val caCrlPath: Path? = null, + // TODO Should be part of a localSigning sub-config + val caCrlUrl: URL? = null, + // TODO Should be part of a localSigning sub-config + val emptyCrlPath: Path? = null, + // TODO Should be part of a localSigning sub-config + val emptyCrlUrl: URL? = null ) { companion object { // TODO: Do we really need these defaults? @@ -53,6 +62,7 @@ data class NetworkManagementServerConfig( // TODO: Move local signing to signing data class DoormanConfig(val approveAll: Boolean = false, @OldConfig("jiraConfig") val jira: JiraConfig? = null, + val crlEndpoint: URL? = null, val approveInterval: Long = NetworkManagementServerConfig.DEFAULT_APPROVE_INTERVAL.toMillis()) { init { require(Booleans.countTrue(approveAll, jira != null) == 1) { @@ -65,6 +75,8 @@ data class CertificateRevocationConfig(val approveAll: Boolean = false, val jira: JiraConfig? = null, val localSigning: LocalSigning?, val crlCacheTimeout: Long, + val caCrlPath: Path, + val emptyCrlPath: Path, val approveInterval: Long = NetworkManagementServerConfig.DEFAULT_APPROVE_INTERVAL.toMillis()) { init { require(Booleans.countTrue(approveAll, jira != null) == 1) { diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/Main.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/Main.kt index 72da06ac1f..fecdcb19f5 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/Main.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/Main.kt @@ -15,6 +15,7 @@ import com.r3.corda.networkmanage.common.utils.* import com.r3.corda.networkmanage.doorman.signer.LocalSigner import net.corda.core.crypto.Crypto import net.corda.core.internal.exists +import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair import net.corda.nodeapi.internal.crypto.X509KeyStore import net.corda.nodeapi.internal.crypto.X509Utilities import org.slf4j.LoggerFactory @@ -36,7 +37,14 @@ fun main(args: Array) { logger.info("Running in ${cmdLineOptions.mode} mode") when (cmdLineOptions.mode) { - Mode.ROOT_KEYGEN -> rootKeyGenMode(cmdLineOptions, config) + Mode.ROOT_KEYGEN -> { + val emptyCrlPath = requireNotNull(config.emptyCrlPath) { "emptyCrlPath needs to be specified" } + val emptyCrlUrl = requireNotNull(config.emptyCrlUrl) { "emptyCrlUrl needs to be specified" } + val caCrlPath = requireNotNull(config.caCrlPath) { "caCrlPath needs to be specified" } + val caCrlUrl = requireNotNull(config.caCrlUrl) { "caCrlUrl needs to be specified" } + val rootCertificateAndKeyPair = rootKeyGenMode(cmdLineOptions, config) + createEmptyCrls(rootCertificateAndKeyPair, emptyCrlPath, emptyCrlUrl, caCrlPath, caCrlUrl) + } Mode.CA_KEYGEN -> caKeyGenMode(config) Mode.DOORMAN -> doormanMode(cmdLineOptions, config) } @@ -61,8 +69,8 @@ private fun processKeyStore(config: NetworkManagementServerConfig): Pair, rejectedRequest: List) { // Reconfirm request status and update jira status + logger.debug("Updating JIRA tickets: `approved` = $approvedRequest, `rejected` = $rejectedRequest") approvedRequest.mapNotNull { crrStorage.getRevocationRequest(it.requestId) } .filter { it.status == RequestStatus.DONE } .forEachWithExceptionLogging(logger) { jiraClient.updateDoneCertificateRevocationRequest(it.requestId) } - rejectedRequest.mapNotNull { crrStorage.getRevocationRequest(it.requestId) } .filter { it.status == RequestStatus.REJECTED } .forEachWithExceptionLogging(logger) { jiraClient.updateRejectedRequest(it.requestId) } diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/CertificateRevocationListWebService.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/CertificateRevocationListWebService.kt index f5b98da759..1f2f4931c7 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/CertificateRevocationListWebService.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/CertificateRevocationListWebService.kt @@ -17,6 +17,8 @@ import javax.ws.rs.core.Response.status @Path(CRL_PATH) class CertificateRevocationListWebService(private val revocationListStorage: CertificateRevocationListStorage, + private val caCrlBytes: ByteArray, + private val emptyCrlBytes: ByteArray, cacheTimeout: Duration) { companion object { private val logger = contextLogger() @@ -24,6 +26,7 @@ class CertificateRevocationListWebService(private val revocationListStorage: Cer const val CRL_DATA_TYPE = "application/pkcs7-crl" const val DOORMAN = "doorman" const val ROOT = "root" + const val EMPTY = "empty" } private val crlCache: LoadingCache = Caffeine.newBuilder() @@ -43,7 +46,14 @@ class CertificateRevocationListWebService(private val revocationListStorage: Cer @Path(ROOT) @Produces(CRL_DATA_TYPE) fun getRootRevocationList(): Response { - return getCrlResponse(CrlIssuer.ROOT) + return ok(caCrlBytes).build() + } + + @GET + @Path(EMPTY) + @Produces(CRL_DATA_TYPE) + fun getEmptyRevocationList(): Response { + return ok(emptyCrlBytes).build() } private fun getCrlResponse(issuer: CrlIssuer): Response { diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/tools/crr/submission/Main.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/tools/crr/submission/Main.kt index 6ffba6acf6..75beedbb5a 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/tools/crr/submission/Main.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/tools/crr/submission/Main.kt @@ -29,7 +29,7 @@ fun submit(url: URL, inputReader: InputReader = ConsoleInputReader()) { val csrRequestId = inputReader.getOptionalInput("certificate signing request ID") val legalName = inputReader.getOptionalInput("node X.500 legal name")?.let { CordaX500Name.parse(it) } CertificateRevocationRequest.validateOptional(certificateSerialNumber, csrRequestId, legalName) - val reason = inputReader.getRequiredInput("revocation reason").let { CRLReason.valueOf(it) } + val reason = getReason(inputReader) val reporter = inputReader.getRequiredInput("reporter of the revocation request") val request = CertificateRevocationRequest(certificateSerialNumber, csrRequestId, legalName, reason, reporter) logger.debug("POST to $url request: $request") @@ -53,4 +53,29 @@ private fun InputReader.getRequiredInput(attributeName: String): String { } else { line } +} + +private enum class SupportedCrlReasons { + UNSPECIFIED, + KEY_COMPROMISE, + CA_COMPROMISE, + AFFILIATION_CHANGED, + SUPERSEDED, + CESSATION_OF_OPERATION, + PRIVILEGE_WITHDRAWN +} + +private fun getReason(inputReader: InputReader): CRLReason { + while (true) { + SupportedCrlReasons.values().forEachIndexed { index, value -> + println("${index + 1}. $value") + } + print("Selected the reason for the revocation:") + val input = inputReader.readLine()!!.toInt() + if (input < 1 || input > SupportedCrlReasons.values().size) { + println("Incorrect selection. Try again.") + } else { + return CRLReason.valueOf(SupportedCrlReasons.values()[input -1 ].name) + } + } } \ No newline at end of file diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/CertificateRevocationListNodeTests.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/CertificateRevocationListNodeTests.kt new file mode 100644 index 0000000000..be614852ca --- /dev/null +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/CertificateRevocationListNodeTests.kt @@ -0,0 +1,108 @@ +package com.r3.corda.networkmanage + +import net.corda.core.toFuture +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX +import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER +import net.corda.nodeapi.internal.crypto.X509KeyStore +import net.corda.nodeapi.internal.protonwrapper.messages.MessageStatus +import net.corda.nodeapi.internal.protonwrapper.netty.AMQPClient +import net.corda.nodeapi.internal.protonwrapper.netty.AMQPServer +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.freePort +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.junit.Before +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.nio.file.Path +import java.nio.file.Paths +import java.security.Security +import kotlin.test.assertEquals + +/** + * This test is to perform manual testing of the SSL connection using local key stores. It aims to assess the + * correct behaviour of the SSL connection between 2 nodes with respect to the CRL validation. + * In order to debug the certificate path validation please use the following JVM parameters when running the test: + * -Djavax.net.debug=ssl,handshake -Djava.security.debug=certpath + */ +@Ignore +class CertificateRevocationListNodeTests { + @Rule + @JvmField + val temporaryFolder = TemporaryFolder() + + private val serverPort = freePort() + + private val serverSslKeyStore: Path = Paths.get("/certificatesServer/sslkeystore.jks") + private val clientSslKeyStore: Path = Paths.get("/certificatesClient/sslkeystore.jks") + private val serverTrustStore: Path = Paths.get("/certificatesServer/truststore.jks") + private val clientTrustStore: Path = Paths.get("/certificatesClient/truststore.jks") + + @Before + fun setUp() { + Security.addProvider(BouncyCastleProvider()) + } + + @Test + fun `Simple AMPQ Client to Server connection works`() { + val amqpServer = createServer(serverPort) + amqpServer.use { + amqpServer.start() + val receiveSubs = amqpServer.onReceive.subscribe { + assertEquals(BOB_NAME.toString(), it.sourceLegalName) + assertEquals(P2P_PREFIX + "Test", it.topic) + assertEquals("Test", String(it.payload)) + it.complete(true) + } + val amqpClient = createClient(serverPort) + amqpClient.use { + val serverConnected = amqpServer.onConnection.toFuture() + val clientConnected = amqpClient.onConnection.toFuture() + amqpClient.start() + val serverConnect = serverConnected.get() + assertEquals(true, serverConnect.connected) + val clientConnect = clientConnected.get() + assertEquals(true, clientConnect.connected) + val msg = amqpClient.createMessage("Test".toByteArray(), + P2P_PREFIX + "Test", + ALICE_NAME.toString(), + emptyMap()) + amqpClient.write(msg) + assertEquals(MessageStatus.Acknowledged, msg.onComplete.get()) + receiveSubs.unsubscribe() + } + } + } + + private fun createClient(targetPort: Int): AMQPClient { + val tS = X509KeyStore.fromFile(clientTrustStore, "trustpass").internal + val sslS = X509KeyStore.fromFile(clientSslKeyStore, "cordacadevpass").internal + return AMQPClient( + listOf(NetworkHostAndPort("localhost", targetPort)), + setOf(ALICE_NAME, CHARLIE_NAME), + PEER_USER, + PEER_USER, + sslS, + "cordacadevpass", + tS, + false) + } + + private fun createServer(port: Int): AMQPServer { + val tS = X509KeyStore.fromFile(serverTrustStore, "trustpass").internal + val sslS = X509KeyStore.fromFile(serverSslKeyStore, "cordacadevpass").internal + return AMQPServer( + "0.0.0.0", + port, + PEER_USER, + PEER_USER, + sslS, + "cordacadevpass", + tS, + false) + } +} diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationListStorageTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationListStorageTest.kt index f0200491c9..7b8bf81b87 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationListStorageTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationListStorageTest.kt @@ -57,6 +57,7 @@ class PersistentCertificateRevocationListStorageTest : TestBase() { certificateSerialNumber = certificate.serialNumber, reason = REVOCATION_REASON, reporter = REPORTER)) + crrStorage.markRequestTicketCreated(requestId) crrStorage.approveRevocationRequest(requestId, "Approver") val revocationRequest = crrStorage.getRevocationRequest(requestId)!! val crl = createDummyCertificateRevocationList(listOf(revocationRequest.certificateSerialNumber)) @@ -80,6 +81,7 @@ class PersistentCertificateRevocationListStorageTest : TestBase() { certificateSerialNumber = createNodeCertificate(csrStorage, legalName = "Bank A").serialNumber, reason = REVOCATION_REASON, reporter = REPORTER)) + crrStorage.markRequestTicketCreated(done) crrStorage.approveRevocationRequest(done, "Approver") val doneRevocationRequest = crrStorage.getRevocationRequest(done)!! @@ -95,6 +97,7 @@ class PersistentCertificateRevocationListStorageTest : TestBase() { certificateSerialNumber = createNodeCertificate(csrStorage, legalName = "Bank C").serialNumber, reason = REVOCATION_REASON, reporter = REPORTER)) + crrStorage.markRequestTicketCreated(approved) crrStorage.approveRevocationRequest(approved, "Approver") val approvedRevocationRequest = crrStorage.getRevocationRequest(approved)!! diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationRequestStorageTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationRequestStorageTest.kt index bdeb2d604f..024799930e 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationRequestStorageTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationRequestStorageTest.kt @@ -85,6 +85,7 @@ class PersistentCertificateRevocationRequestStorageTest : TestBase() { certificateSerialNumber = createNodeCertificate(csrStorage, "LegalName" + it.toString()).serialNumber, reason = REVOCATION_REASON, reporter = REPORTER)) + crrStorage.markRequestTicketCreated(requestId) crrStorage.approveRevocationRequest(requestId, "Approver") } @@ -117,6 +118,7 @@ class PersistentCertificateRevocationRequestStorageTest : TestBase() { certificateSerialNumber = certificate.serialNumber, reason = REVOCATION_REASON, reporter = REPORTER)) + crrStorage.markRequestTicketCreated(requestId) // when crrStorage.approveRevocationRequest(requestId, "Approver") @@ -135,6 +137,7 @@ class PersistentCertificateRevocationRequestStorageTest : TestBase() { certificateSerialNumber = certificate.serialNumber, reason = REVOCATION_REASON, reporter = REPORTER)) + crrStorage.markRequestTicketCreated(requestId) // when crrStorage.rejectRevocationRequest(requestId, "Rejector", "No reason") diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/tools/crr/submission/CertificateRevocationRequestSubmissionToolTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/tools/crr/submission/CertificateRevocationRequestSubmissionToolTest.kt index f3eb5301f6..793cf4b0ef 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/tools/crr/submission/CertificateRevocationRequestSubmissionToolTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/tools/crr/submission/CertificateRevocationRequestSubmissionToolTest.kt @@ -65,7 +65,7 @@ class CertificateRevocationRequestSubmissionToolTest { givenUserConsoleSequentialInputOnReadLine(request.certificateSerialNumber.toString(), request.csrRequestId!!, request.legalName.toString(), - request.reason.name, + "${request.reason.ordinal + 1}", request.reporter) val requestId = SecureHash.randomSHA256().toString() diff --git a/node/build.gradle b/node/build.gradle index 7800ff24c5..a51b525344 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -71,6 +71,8 @@ dependencies { compile project(":confidential-identities") compile project(':client:rpc') compile project(':tools:shell') + runtime project(':launcher') + compile "net.corda.plugins:cordform-common:$gradle_plugins_version" // Log4J: logging framework (with SLF4J bindings) diff --git a/node/capsule/build.gradle b/node/capsule/build.gradle index aab294cda1..e4e8984eb3 100644 --- a/node/capsule/build.gradle +++ b/node/capsule/build.gradle @@ -28,6 +28,11 @@ dependencies { capsuleRuntime "com.typesafe:config:$typesafe_config_version" } +ext { + quasarExcludeExpression = "x(antlr**;bftsmart**;ch**;co.paralleluniverse**;com.codahale**;com.esotericsoftware**;com.fasterxml**;com.google**;com.ibm**;com.intellij**;com.jcabi**;com.nhaarman**;com.opengamma**;com.typesafe**;com.zaxxer**;de.javakaffee**;groovy**;groovyjarjarantlr**;groovyjarjarasm**;io.atomix**;io.github**;io.netty**;jdk**;joptsimple**;junit**;kotlin**;net.bytebuddy**;net.i2p**;org.apache**;org.assertj**;org.bouncycastle**;org.codehaus**;org.crsh**;org.dom4j**;org.fusesource**;org.h2**;org.hamcrest**;org.hibernate**;org.jboss**;org.jcp**;org.joda**;org.junit**;org.mockito**;org.objectweb**;org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**;org.jolokia**)" + applicationClass = 'net.corda.node.Corda' +} + // Force the Caplet to target Java 6. This ensures that running 'java -jar corda.jar' on any Java 6 VM upwards // will get as far as the Capsule version checks, meaning that if your JVM is too old, you will at least get // a sensible error message telling you what to do rather than a bytecode version exception that doesn't. @@ -37,8 +42,8 @@ sourceCompatibility = 1.6 targetCompatibility = 1.6 task buildCordaJAR(type: FatCapsule, dependsOn: project(':node').compileJava) { - applicationClass 'net.corda.node.Corda' archiveName "corda-r3-${corda_release_version}.jar" + applicationClass 'net.corda.node.Corda' applicationSource = files( project(':node').configurations.runtime, project(':node').jar, @@ -54,7 +59,6 @@ task buildCordaJAR(type: FatCapsule, dependsOn: project(':node').compileJava) { applicationVersion = corda_release_version // See experimental/quasar-hook/README.md for how to generate. - def quasarExcludeExpression = "x(antlr**;bftsmart**;ch**;co.paralleluniverse**;com.codahale**;com.esotericsoftware**;com.fasterxml**;com.google**;com.ibm**;com.intellij**;com.jcabi**;com.nhaarman**;com.opengamma**;com.typesafe**;com.zaxxer**;de.javakaffee**;groovy**;groovyjarjarantlr**;groovyjarjarasm**;io.atomix**;io.github**;io.netty**;jdk**;joptsimple**;junit**;kotlin**;net.bytebuddy**;net.i2p**;org.apache**;org.assertj**;org.bouncycastle**;org.codehaus**;org.crsh**;org.dom4j**;org.fusesource**;org.h2**;org.hamcrest**;org.hibernate**;org.jboss**;org.jcp**;org.joda**;org.junit**;org.mockito**;org.objectweb**;org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**;org.jolokia**)" javaAgents = ["quasar-core-${quasar_version}-jdk8.jar=${quasarExcludeExpression}"] systemProperties['visualvm.display.name'] = 'CordaEnterprise' minJavaVersion = '1.8.0' diff --git a/node/dist/README.md b/node/dist/README.md new file mode 100644 index 0000000000..cea059c4d9 --- /dev/null +++ b/node/dist/README.md @@ -0,0 +1,5 @@ +This project adds `buildCordaTarball` task to Gradle. It prepares distributable tarball with JRE built-in, using ``javapackager`` + +For now, it packs the whatever JRE is available in the system, but this will get standarised over time. + +It requires ``javapackager`` to be available in the path. \ No newline at end of file diff --git a/node/dist/build.gradle b/node/dist/build.gradle new file mode 100644 index 0000000000..920e5c3e00 --- /dev/null +++ b/node/dist/build.gradle @@ -0,0 +1,159 @@ +description 'Package Node as stand-alone application' + +evaluationDependsOn(":node") +evaluationDependsOn(":docs") +evaluationDependsOn(":launcher") + +ext { + outputDir = "$buildDir/corda" +} + +def tmpDir = "${buildDir}/tmp/" + +configurations { + launcherClasspath +} + +// Define location of predefined startup scripts, license and README files +sourceSets { + binFiles { + resources { + srcDir file('src/main/resources/bin') + } + } + readmeFiles { + resources { + srcDir file('src/main/resources/readme') + } + } +} + +// Runtime dependencies of launcher +dependencies { + compile project(':node') + launcherClasspath project(':launcher') + launcherClasspath "org.slf4j:jul-to-slf4j:$slf4j_version" + launcherClasspath "org.apache.logging.log4j:log4j-slf4j-impl:${log4j_version}" + launcherClasspath "org.apache.logging.log4j:log4j-web:${log4j_version}" + + // Required by JVM agents: + launcherClasspath "com.google.guava:guava:$guava_version" + launcherClasspath "de.javakaffee:kryo-serializers:0.41" +} + +task copyLauncherLibs(type: Copy, dependsOn: [project(':launcher').jar]) { + from configurations.launcherClasspath + into "$buildDir/tmp/launcher-lib" +} + +// The launcher building is done as it depends on application-specific settings which +// cannot be overridden later +task buildLauncher(type: Exec, dependsOn: [copyLauncherLibs]) { + description 'Build Launcher executable' + + def isLinux = System.properties['os.name'].toLowerCase().contains('linux') + def isMac = System.properties['os.name'].toLowerCase().contains('mac') + + if (!isLinux && !isMac) + throw new GradleException("Preparing distribution package is currently only supported on Linux/Mac") + + def relativeDir + if (isLinux) + relativeDir = "launcher" + else + relativeDir = "launcher.app/Contents" + + def extraArgs = [ + "-BjvmOptions=-javaagent:../../lib/quasar-core-${quasar_version}-jdk8.jar=${project(':node:capsule').quasarExcludeExpression}", + '-BuserJvmOptions=-Xmx=4g', + '-BuserJvmOptions=-XX\\:=+UseG1GC', + "-BjvmProperties=java.system.class.loader=${project(':launcher').loaderClassName}" + ] + + ext { + launcherBinDir = "${tmpDir}/bundles/$relativeDir" + } + + workingDir project.projectDir + + doFirst { + def launcherLib = copyLauncherLibs.destinationDir + def srcfiles = [] + def launcherClasspath = [] + + fileTree(launcherLib).forEach({ file -> + srcfiles.add("-srcfiles") + srcfiles.add(file.name) + launcherClasspath.add(file.name) + }) + + commandLine = [ + 'javapackager', + '-deploy', + '-nosign', + '-native', 'image', + '-outdir', "$tmpDir", + '-outfile', 'launcher', + '-name', 'launcher', + "-BmainJar=${project(':launcher').jar.archiveName}", + "-Bclasspath=${launcherClasspath.join(":")}", + '-appclass', "${project(':launcher').launcherClassName}", + '-srcdir', "$launcherLib" + ] + srcfiles + extraArgs + } + + // Add configuration for running Node application + doLast { + def nodeClasspath = [] + def libRelPath = "../lib/" + + project(':node').configurations.runtime.forEach({ file -> + nodeClasspath.add(file.getName()) + }) + + nodeClasspath.add(project(':node').jar.archivePath.getName()) + + new File("${launcherBinDir}/runtime.properties").text = [ + "libpath=${libRelPath}", + "classpath=${nodeClasspath.join(':')}", + "plugins=./drivers:./cordapps"].join("\n") + } +} + +task buildNode(type: Copy, dependsOn: [buildLauncher, project(':docs').tasks['makeDocs'], project(':node').tasks['jar']]) { + description 'Build stand-alone Corda Node distribution' + + into(outputDir) + + from(buildLauncher.launcherBinDir) { + into("launcher") + } + + from(project(':node').configurations.runtime) { + into("lib") + } + + from(project(':node').jar.archivePath) { + into("lib") + } + + from(sourceSets.binFiles.resources) { + into("bin") + } + + from(sourceSets.readmeFiles.resources) { + into(".") + } + + from(project(':docs').buildDir) { + into("docs") + } + + doLast { + new File("${outputDir}/cordapps").mkdirs() + new File("${outputDir}/drivers").mkdirs() + println ("Stand-alone Corda Node application available at:") + println ("${outputDir}") + } +} + diff --git a/node/dist/src/main/resources/bin/corda b/node/dist/src/main/resources/bin/corda new file mode 100755 index 0000000000..ad5ce399d7 --- /dev/null +++ b/node/dist/src/main/resources/bin/corda @@ -0,0 +1,20 @@ +#!/bin/sh +# ------------------------ +# Corda startup script +# ------------------------- + +MAINCLASSNAME="net.corda.node.Corda" +READLINK=`which readlink` + +# Locate this script and relative launcher executable +SCRIPT_LOCATION=$0 +if [ -x "$READLINK" ]; then + while [ -L "$SCRIPT_LOCATION" ]; do + SCRIPT_LOCATION=`"$READLINK" -e "$SCRIPT_LOCATION"` + done +fi +SCRIPT_DIR=`dirname "$SCRIPT_LOCATION"` +LAUNCHER_LOCATION="$SCRIPT_DIR/../launcher/launcher" + +# Run Corda +CORDA_LAUNCHER_CWD="`pwd`" ${LAUNCHER_LOCATION} ${MAINCLASSNAME} "$@" diff --git a/node/dist/src/main/resources/readme/README b/node/dist/src/main/resources/readme/README new file mode 100644 index 0000000000..70ebbc74fd --- /dev/null +++ b/node/dist/src/main/resources/readme/README @@ -0,0 +1,9 @@ +Welcome to Corda Enterprise! + +This is a distributon package containing the Java Runtime Environment for convenience. + +To start a node, please edit supplied node.conf file so it contains appropriate data for your organization. More - https://docs.corda.net/corda-configuration-file.html + +Your CordApps should be placed in cordapps directory, from which they will be loaded automatically. + +Linux/Mac: main executable file is ./bin/corda diff --git a/node/dist/src/main/resources/readme/example-node.conf b/node/dist/src/main/resources/readme/example-node.conf new file mode 100644 index 0000000000..15d45001d7 --- /dev/null +++ b/node/dist/src/main/resources/readme/example-node.conf @@ -0,0 +1,61 @@ +p2pAddress="localhost:10005" +myLegalName="O=Bank A,L=London,C=GB" +emailAddress = "admin@company.com" +keyStorePassword = "cordacadevpass" +trustStorePassword = "trustpass" +dataSourceProperties = { + dataSourceClassName = org.h2.jdbcx.JdbcDataSource + dataSource.url = "jdbc:h2:file:"${baseDirectory}"/persistence;DB_CLOSE_ON_EXIT=FALSE;LOCK_TIMEOUT=10000;WRITE_DELAY=0;AUTO_SERVER_PORT="${h2port} + dataSource.user = sa + dataSource.password = "" +} +database = { + transactionIsolationLevel = "REPEATABLE_READ" + exportHibernateJMXStatistics = "false" +} +devMode = true +h2port = 0 +useTestClock = false +verifierType = InMemory +rpcSettings = { + useSsl = false + standAloneBroker = false + address="localhost:10007" + adminAddress="localhost:10008" +} +security { + authService { + dataSource { + type=INMEMORY + users=[ + { + password=default + permissions=[ + ALL + ] + username=default + } + ] + } + } +} +enterpriseConfiguration = { + mutualExclusionConfiguration = { + on = false + machineName = "" + updateInterval = 20000 + waitInterval = 40000 + } + tuning = { + flowThreadPoolSize = 1 + rpcThreadPoolSize = 4 + maximumMessagingBatchSize = 256 + p2pConfirmationWindowSize = 1048576 + brokerConnectionTtlCheckIntervalMs = 20 + stateMachine = { + eventQueueSize = 16 + sessionDeliverPersistenceStrategy = "OnNextCommit" + } + } + useMultiThreadedSMM = true +} diff --git a/node/src/main/kotlin/net/corda/node/NodeArgsParser.kt b/node/src/main/kotlin/net/corda/node/NodeArgsParser.kt index 4e885beea9..babd44a57e 100644 --- a/node/src/main/kotlin/net/corda/node/NodeArgsParser.kt +++ b/node/src/main/kotlin/net/corda/node/NodeArgsParser.kt @@ -68,7 +68,12 @@ class NodeArgsParser : AbstractArgsParser() { require(!optionSet.has(baseDirectoryArg) || !optionSet.has(configFileArg)) { "${baseDirectoryArg.options()[0]} and ${configFileArg.options()[0]} cannot be specified together" } - val baseDirectory = optionSet.valueOf(baseDirectoryArg).normalize().toAbsolutePath() + // Workaround for javapackager polluting cwd: restore it from system property set by launcher. + val baseDirectory = System.getProperty("corda.launcher.cwd")?.let { Paths.get(it) } + ?: optionSet.valueOf(baseDirectoryArg) + .normalize() + .toAbsolutePath() + val configFile = baseDirectory / optionSet.valueOf(configFileArg) val loggingLevel = optionSet.valueOf(loggerLevel) val logToConsole = optionSet.has(logToConsoleArg) diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt index 0aaf718546..e3184bb355 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt @@ -29,6 +29,7 @@ import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.tools.shell.SSHDConfiguration import org.bouncycastle.asn1.x500.X500Name import org.slf4j.Logger +import sun.security.x509.X500Name import java.net.URL import java.nio.file.Path import java.time.Duration diff --git a/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt b/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt index e6c76aa71f..cfaf33b036 100644 --- a/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt @@ -24,6 +24,7 @@ import net.corda.tools.shell.SSHDConfiguration import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.Test +import java.net.URL import java.net.URI import java.net.URL import java.nio.file.Paths diff --git a/settings.gradle b/settings.gradle index 82ada2052d..9b83f51f66 100644 --- a/settings.gradle +++ b/settings.gradle @@ -56,6 +56,7 @@ include 'tools:explorer' include 'tools:explorer:capsule' include 'tools:demobench' include 'tools:loadtest' +include 'tools:notarytest' include 'tools:graphs' include 'tools:bootstrapper' include 'tools:dbmigration' @@ -82,3 +83,5 @@ project(':hsm-tool').with { name = 'sgx-hsm-tool' projectDir = file("$settingsDir/sgx-jvm/hsm-tool") } +include 'launcher' +include 'node:dist' diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalTestUtils.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalTestUtils.kt index 710d246cd5..fb819be221 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalTestUtils.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalTestUtils.kt @@ -151,9 +151,9 @@ internal interface InternalMockMessagingService : MessagingService { * @param fallBackConfigSupplier Returns [Config] with dataSourceProperties, invoked with [nodeName] and [nodeNameExtension] parameters. * Defaults to configuration of in-memory H2 instance. */ -fun makeTestDataSourceProperties(nodeName: String = SecureHash.randomSHA256().toString(), +fun makeTestDataSourceProperties(nodeName: String? = SecureHash.randomSHA256().toString(), nodeNameExtension: String? = null, - configSupplier: (String, String?) -> Config = ::databaseProviderDataSourceConfig, + configSupplier: (String?, String?) -> Config = ::databaseProviderDataSourceConfig, fallBackConfigSupplier: (String?, String?) -> Config = ::inMemoryH2DataSourceConfig): Properties { val config = configSupplier(nodeName, nodeNameExtension) .withFallback(fallBackConfigSupplier(nodeName, nodeNameExtension)) @@ -172,9 +172,11 @@ fun makeTestDataSourceProperties(nodeName: String = SecureHash.randomSHA256().to * Make properties appropriate for creating a Database for unit tests. * * @param nodeName Reflects the "instance" of the in-memory database or database username/schema. + * @param configSupplier Returns [Config] with databaseProperties, invoked with [nodeName] parameter. */ -fun makeTestDatabaseProperties(nodeName: String? = null): DatabaseConfig { - val config = databaseProviderDataSourceConfig(nodeName) +fun makeTestDatabaseProperties(nodeName: String? = null, + configSupplier: (String?, String?) -> Config = ::databaseProviderDataSourceConfig): DatabaseConfig { + val config = configSupplier(nodeName, null) val transactionIsolationLevel = if (config.hasPath(DatabaseConstants.TRANSACTION_ISOLATION_LEVEL)) TransactionIsolationLevel.valueOf(config.getString(DatabaseConstants.TRANSACTION_ISOLATION_LEVEL)) else TransactionIsolationLevel.READ_COMMITTED diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt index 4c140b744e..87baa6449f 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt @@ -10,7 +10,6 @@ package net.corda.testing.internal -import com.nhaarman.mockito_kotlin.doAnswer import net.corda.core.crypto.Crypto import net.corda.core.crypto.Crypto.generateKeyPair import net.corda.core.identity.CordaX500Name diff --git a/tools/notarytest/README.md b/tools/notarytest/README.md new file mode 100644 index 0000000000..d369668d5e --- /dev/null +++ b/tools/notarytest/README.md @@ -0,0 +1,7 @@ +# Notary test tool + +Provides building blocks for experimenting and load testing new notary implementations: +* Deploying a custom notary service locally. +* Invoking a load generating flow on a notary node. + +No scripts for deploying nodes to a remote test cluster are provided. \ No newline at end of file diff --git a/tools/notarytest/build.gradle b/tools/notarytest/build.gradle new file mode 100644 index 0000000000..98191d76bd --- /dev/null +++ b/tools/notarytest/build.gradle @@ -0,0 +1,61 @@ +import net.corda.plugins.Cordform + +apply plugin: 'java' +apply plugin: 'kotlin' +apply plugin: 'idea' +apply plugin: 'net.corda.plugins.quasar-utils' +apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'net.corda.plugins.cordapp' +apply plugin: 'net.corda.plugins.cordformation' +apply plugin: 'maven-publish' + +dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" + testCompile "junit:junit:$junit_version" + + // Corda integration dependencies + cordaCompile project(path: ":node:capsule", configuration: 'runtimeArtifacts') + cordaCompile project(':core') + cordaCompile project(':client:rpc') + cordaCompile project(':node-driver') + compile project(':client:mock') + compile group: 'mysql', name: 'mysql-connector-java', version: '6.0.6' + compile group: 'io.dropwizard.metrics', name: 'metrics-graphite', version: '3.2.5' + +} + +idea { + module { + downloadJavadoc = true // defaults to false + downloadSources = true + } +} + +publishing { + publications { + jarAndSources(MavenPublication) { + from components.java + artifactId 'notarytest' + + artifact sourceJar + artifact javadocJar + } + } +} + +task deployJDBC(type: Cordform, dependsOn: 'jar') { + definitionClass = 'net.corda.notarytest.JDBCNotaryCordform' +} + +task runTest(type: JavaExec) { + classpath = sourceSets.main.runtimeClasspath + main = 'net.corda.notarytest.MainKt' +} + +jar { + manifest { + attributes( + 'Automatic-Module-Name': 'net.corda.notarytest' + ) + } +} diff --git a/tools/notarytest/src/main/kotlin/net/corda/notarytest/JDBCNotaryCordform.kt b/tools/notarytest/src/main/kotlin/net/corda/notarytest/JDBCNotaryCordform.kt new file mode 100644 index 0000000000..47489a1cc3 --- /dev/null +++ b/tools/notarytest/src/main/kotlin/net/corda/notarytest/JDBCNotaryCordform.kt @@ -0,0 +1,82 @@ +package net.corda.notarytest + +import net.corda.cordform.CordformContext +import net.corda.cordform.CordformDefinition +import net.corda.cordform.CordformNode +import net.corda.core.identity.CordaX500Name +import net.corda.node.services.Permissions +import net.corda.node.services.config.NotaryConfig +import net.corda.nodeapi.internal.DevIdentityGenerator +import net.corda.testing.node.User +import net.corda.testing.node.internal.demorun.* + +fun main(args: Array) = JDBCNotaryCordform().nodeRunner().deployAndRunNodes() + +internal val notaryDemoUser = User("demou", "demop", setOf(Permissions.all())) + +class JDBCNotaryCordform : CordformDefinition() { + private val clusterName = CordaX500Name("Mysql Notary", "Zurich", "CH") + private val notaryNames = createNotaryNames(3) + + private fun createNotaryNames(clusterSize: Int) = (0 until clusterSize).map { + CordaX500Name("Notary Service $it", "Zurich", "CH") + } + + init { + fun notaryNode(index: Int, configure: CordformNode.() -> Unit) = node { + name(notaryNames[index]) + notary( + NotaryConfig( + validating = true, + custom = true + ) + ) + extraConfig = mapOf("custom" to + mapOf( + "mysql" to mapOf( + "dataSource" to mapOf( + // Update the db address/port accordingly + "jdbcUrl" to "jdbc:mysql://localhost:330${6 + index}/corda?rewriteBatchedStatements=true&useSSL=false&failOverReadOnly=false", + "username" to "corda", + "password" to "awesome", + "autoCommit" to "false") + ), + "graphiteAddress" to "performance-metrics.northeurope.cloudapp.azure.com:2004" + ) + ) + configure() + } + + notaryNode(0) { + p2pPort(10009) + rpcSettings { + address("localhost:10010") + adminAddress("localhost:10110") + } + rpcUsers(notaryDemoUser) + } + notaryNode(1) { + p2pPort(10013) + rpcSettings { + address("localhost:10014") + adminAddress("localhost:10114") + } + rpcUsers(notaryDemoUser) + } + notaryNode(2) { + p2pPort(10017) + rpcSettings { + address("localhost:10018") + adminAddress("localhost:10118") + } + rpcUsers(notaryDemoUser) + } + } + + override fun setup(context: CordformContext) { + DevIdentityGenerator.generateDistributedNotarySingularIdentity( + notaryNames.map { context.baseDirectory(it.toString()) }, + clusterName + ) + } +} \ No newline at end of file diff --git a/tools/notarytest/src/main/kotlin/net/corda/notarytest/Main.kt b/tools/notarytest/src/main/kotlin/net/corda/notarytest/Main.kt new file mode 100644 index 0000000000..b966f6fbd6 --- /dev/null +++ b/tools/notarytest/src/main/kotlin/net/corda/notarytest/Main.kt @@ -0,0 +1,67 @@ +package net.corda.notarytest + +import com.google.common.base.Stopwatch +import net.corda.client.rpc.CordaRPCClient +import net.corda.core.messaging.CordaRPCOps +import net.corda.core.messaging.startFlow +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.notarytest.service.JDBCLoadTestFlow +import java.io.File +import java.io.PrintWriter +import java.time.Instant +import java.util.concurrent.TimeUnit + +/** The number of test flows to run on each notary node */ +const val TEST_RUNS = 1 +/** Total number of transactions to generate and notarise. */ +const val TRANSACTION_COUNT = 10000000 +/** Number of transactions to submit before awaiting completion. */ +const val BATCH_SIZE = 1000 + +fun main(args: Array) { + // Provide a list of notary node addresses to invoke the load generation flow on + val addresses = listOf( + NetworkHostAndPort("localhost", 10010), + NetworkHostAndPort("localhost", 11014), + NetworkHostAndPort("localhost", 11018) + ) + + addresses.parallelStream().forEach { + val node = it + println("Connecting to the recipient node ($node)") + + CordaRPCClient(it).start(notaryDemoUser.username, notaryDemoUser.password).use { + println(it.proxy.nodeInfo()) + val totalTime = Stopwatch.createStarted() + val durations = run(it.proxy, 1) + totalTime.stop() + + val totalTx = TEST_RUNS * TRANSACTION_COUNT + println("Total duration for $totalTx transactions: ${totalTime.elapsed(TimeUnit.MILLISECONDS)} ms") + println("Average tx/s: ${totalTx.toDouble() / totalTime.elapsed(TimeUnit.MILLISECONDS).toDouble() * 1000}") + + // Uncomment to generate a CSV report + // printCSV(node, durations, TEST_RUNS, BATCH_SIZE) + } + } +} + +private fun run(rpc: CordaRPCOps, inputStateCount: Int? = null): List { + return (1..TEST_RUNS).map { i -> + val timer = Stopwatch.createStarted() + val commitDuration = rpc.startFlow(::JDBCLoadTestFlow, TRANSACTION_COUNT, BATCH_SIZE, inputStateCount).returnValue.get() + val flowDuration = timer.stop().elapsed(TimeUnit.MILLISECONDS) + println("#$i: Duration: $flowDuration ms, commit duration: $commitDuration ms") + flowDuration + } +} + +private fun printCSV(node: NetworkHostAndPort, durations: List, testRuns: Int, batchSize: Int) { + val pw = PrintWriter(File("notarytest-${Instant.now()}-${node.host}${node.port}-${testRuns}x$batchSize.csv")) + val sb = StringBuilder() + sb.append("$testRuns, $batchSize") + sb.append('\n') + sb.append(durations.joinToString()) + pw.write(sb.toString()) + pw.close() +} \ No newline at end of file diff --git a/tools/notarytest/src/main/kotlin/net/corda/notarytest/flows/AsyncLoadTestFlow.kt b/tools/notarytest/src/main/kotlin/net/corda/notarytest/flows/AsyncLoadTestFlow.kt new file mode 100644 index 0000000000..c6d272637c --- /dev/null +++ b/tools/notarytest/src/main/kotlin/net/corda/notarytest/flows/AsyncLoadTestFlow.kt @@ -0,0 +1,86 @@ +package net.corda.notarytest.flows + +import co.paralleluniverse.fibers.Suspendable +import com.google.common.base.Stopwatch +import net.corda.client.mock.Generator +import net.corda.core.concurrent.CordaFuture +import net.corda.core.contracts.StateRef +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.entropyToKeyPair +import net.corda.core.crypto.generateKeyPair +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.NotarisationRequest +import net.corda.core.flows.NotarisationRequestSignature +import net.corda.core.flows.StartableByRPC +import net.corda.core.identity.CordaX500Name +import net.corda.core.identity.Party +import net.corda.core.internal.concurrent.transpose +import net.corda.core.internal.notary.AsyncCFTNotaryService +import net.corda.core.internal.notary.AsyncUniquenessProvider +import net.corda.core.internal.notary.generateSignature +import java.math.BigInteger +import java.util.* +import java.util.concurrent.TimeUnit + +@StartableByRPC +open class AsyncLoadTestFlow( + private val serviceType: Class, + private val transactionCount: Int, + private val batchSize: Int = 100, + /** + * Number of input states per transaction. + * If *null*, variable sized transactions will be created with median size 4. + */ + private val inputStateCount: Int? = null +) : FlowLogic() { + private val keyPairGenerator = Generator.long().map { entropyToKeyPair(BigInteger.valueOf(it)) } + private val publicKeyGeneratorSingle = Generator.pure(generateKeyPair().public) + private val partyGenerator: Generator = Generator.int().combine(publicKeyGeneratorSingle) { n, key -> + Party(CordaX500Name(organisation = "Party$n", locality = "London", country = "GB"), key) + } + private val txIdGenerator = Generator.bytes(32).map { SecureHash.sha256(it) } + private val stateRefGenerator = txIdGenerator.combine(Generator.intRange(0, 10)) { id, pos -> StateRef(id, pos) } + + @Suspendable + override fun call(): Long { + var current = 0 + var totalDuration = 0L + while (current < transactionCount) { + val batch = Math.min(batchSize, transactionCount - current) + totalDuration += runBatch(batch) + current += batch + } + return totalDuration + } + + private val random = SplittableRandom() + + private fun runBatch(transactionCount: Int): Long { + val stopwatch = Stopwatch.createStarted() + val futures = mutableListOf>() + + val service = serviceHub.cordaService(serviceType) + + for (i in 1..batchSize) { + val txId: SecureHash = txIdGenerator.generateOrFail(random) + val callerParty = partyGenerator.generateOrFail(random) + val inputGenerator = if (inputStateCount == null) { + Generator.replicatePoisson(4.0, stateRefGenerator, true) + } else { + Generator.replicate(inputStateCount, stateRefGenerator) + } + val inputs = inputGenerator.generateOrFail(random) + val requestSignature = NotarisationRequest(inputs, txId).generateSignature(serviceHub) + + futures += AsyncCFTNotaryService.CommitOperation(service, inputs, txId, callerParty, requestSignature, null).execute() + } + + futures.transpose().get() + + stopwatch.stop() + val duration = stopwatch.elapsed(TimeUnit.MILLISECONDS) + logger.info("Committed $transactionCount transactions in $duration ms, avg ${duration.toDouble() / transactionCount} ms") + + return duration + } +} \ No newline at end of file diff --git a/tools/notarytest/src/main/kotlin/net/corda/notarytest/flows/LoadTestFlow.kt b/tools/notarytest/src/main/kotlin/net/corda/notarytest/flows/LoadTestFlow.kt new file mode 100644 index 0000000000..b0b78e084e --- /dev/null +++ b/tools/notarytest/src/main/kotlin/net/corda/notarytest/flows/LoadTestFlow.kt @@ -0,0 +1,67 @@ +package net.corda.notarytest.flows + +import co.paralleluniverse.fibers.Suspendable +import com.google.common.base.Stopwatch +import net.corda.client.mock.Generator +import net.corda.core.contracts.StateRef +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.entropyToKeyPair +import net.corda.core.crypto.generateKeyPair +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.NotarisationRequest +import net.corda.core.flows.StartableByRPC +import net.corda.core.identity.CordaX500Name +import net.corda.core.identity.Party +import net.corda.core.internal.notary.TrustedAuthorityNotaryService +import net.corda.core.internal.notary.generateSignature +import java.math.BigInteger +import java.util.* +import java.util.concurrent.TimeUnit + +@StartableByRPC +open class LoadTestFlow( + private val serviceType: Class, + private val transactionCount: Int, + /** + * Number of input states per transaction. + * If *null*, variable sized transactions will be created with median size 4. + */ + private val inputStateCount: Int? +) : FlowLogic() { + private val keyPairGenerator = Generator.long().map { entropyToKeyPair(BigInteger.valueOf(it)) } + private val publicKeyGenerator = keyPairGenerator.map { it.public } + + private val publicKeyGenerator2 = Generator.pure(generateKeyPair().public) + private val partyGenerator: Generator = Generator.int().combine(publicKeyGenerator2) { n, key -> + Party(CordaX500Name(organisation = "Party$n", locality = "London", country = "GB"), key) + } + private val txIdGenerator = Generator.bytes(32).map { SecureHash.sha256(it) } + private val stateRefGenerator = Generator.intRange(0, 10).map { StateRef(SecureHash.randomSHA256(), it) } + + @Suspendable + override fun call(): Long { + val stopwatch = Stopwatch.createStarted() + val random = SplittableRandom() + + for (i in 1..transactionCount) { + val txId: SecureHash = txIdGenerator.generateOrFail(random) + val callerParty = partyGenerator.generateOrFail(random) + val inputGenerator = if (inputStateCount == null) { + Generator.replicatePoisson(4.0, stateRefGenerator, true) + } else { + Generator.replicate(inputStateCount, stateRefGenerator) + } + val inputs = inputGenerator.generateOrFail(random) + val localStopwatch = Stopwatch.createStarted() + val sig = NotarisationRequest(inputs, txId).generateSignature(serviceHub) + serviceHub.cordaService(serviceType).commitInputStates(inputs, txId, callerParty, sig, null) + logger.info("Committed a transaction ${txId.toString().take(10)} with ${inputs.size} inputs in ${localStopwatch.stop().elapsed(TimeUnit.MILLISECONDS)} ms") + } + + stopwatch.stop() + val duration = stopwatch.elapsed(TimeUnit.MILLISECONDS) + logger.info("Committed $transactionCount transactions in $duration, avg ${duration.toDouble() / transactionCount} ms") + + return duration + } +} \ No newline at end of file diff --git a/tools/notarytest/src/main/kotlin/net/corda/notarytest/service/JDBCNotaryService.kt b/tools/notarytest/src/main/kotlin/net/corda/notarytest/service/JDBCNotaryService.kt new file mode 100644 index 0000000000..b81ec0863a --- /dev/null +++ b/tools/notarytest/src/main/kotlin/net/corda/notarytest/service/JDBCNotaryService.kt @@ -0,0 +1,73 @@ +package net.corda.notarytest.service + +import com.codahale.metrics.MetricFilter +import com.codahale.metrics.MetricRegistry +import com.codahale.metrics.graphite.GraphiteReporter +import com.codahale.metrics.graphite.PickledGraphite +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowSession +import net.corda.core.flows.StartableByRPC +import net.corda.core.internal.notary.AsyncCFTNotaryService +import net.corda.core.node.AppServiceHub +import net.corda.core.node.services.CordaService +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.node.services.config.ConfigHelper +import net.corda.node.services.config.MySQLConfiguration +import net.corda.node.services.transactions.MySQLUniquenessProvider +import net.corda.node.services.transactions.NonValidatingNotaryFlow +import net.corda.nodeapi.internal.config.parseAs +import net.corda.notarytest.flows.AsyncLoadTestFlow +import java.net.InetAddress +import java.net.InetSocketAddress +import java.nio.file.Paths +import java.security.PublicKey +import java.util.concurrent.TimeUnit + +@CordaService +class JDBCNotaryService(override val services: AppServiceHub, override val notaryIdentityKey: PublicKey) : AsyncCFTNotaryService() { + private val appConfig = ConfigHelper.loadConfig(Paths.get(".")).getConfig("custom") + + override val asyncUniquenessProvider: MySQLUniquenessProvider = createUniquenessProvider() + + override fun createServiceFlow(otherPartySession: FlowSession): FlowLogic = NonValidatingNotaryFlow(otherPartySession, this) + + override fun start() { + asyncUniquenessProvider.createTable() + } + + override fun stop() { + asyncUniquenessProvider.stop() + } + + private fun createMetricsRegistry(): MetricRegistry { + val graphiteAddress = appConfig.getString("graphiteAddress").let { NetworkHostAndPort.parse(it) } + val hostName = InetAddress.getLocalHost().hostName.replace(".", "_") + val nodeName = services.myInfo.legalIdentities.first().name.organisation + .toLowerCase() + .replace(" ", "_") + .replace(".", "_") + val pickledGraphite = PickledGraphite( + InetSocketAddress(graphiteAddress.host, graphiteAddress.port) + ) + val metrics = MetricRegistry() + GraphiteReporter.forRegistry(metrics) + .prefixedWith("corda.$hostName.$nodeName") + .convertRatesTo(TimeUnit.SECONDS) + .convertDurationsTo(TimeUnit.MILLISECONDS) + .filter(MetricFilter.ALL) + .build(pickledGraphite) + .start(10, TimeUnit.SECONDS) + return metrics + } + + private fun createUniquenessProvider(): MySQLUniquenessProvider { + val mysqlConfig = appConfig.getConfig("mysql").parseAs() + return MySQLUniquenessProvider(createMetricsRegistry(), services.clock, mysqlConfig) + } +} + +@StartableByRPC +class JDBCLoadTestFlow(transactionCount: Int, + batchSize: Int, + inputStateCount: Int? +) : AsyncLoadTestFlow(JDBCNotaryService::class.java, transactionCount, batchSize, inputStateCount) \ No newline at end of file