mirror of
https://github.com/corda/corda.git
synced 2025-03-14 00:06:45 +00:00
Merge branch 'master' into kat-merge-20180517
This commit is contained in:
commit
3778f029df
8
.idea/compiler.xml
generated
8
.idea/compiler.xml
generated
@ -52,6 +52,12 @@
|
||||
<module name="core_test" target="1.8" />
|
||||
<module name="demobench_main" target="1.8" />
|
||||
<module name="demobench_test" target="1.8" />
|
||||
<module name="dist_binFiles" target="1.8" />
|
||||
<module name="dist_licenseFiles" target="1.8" />
|
||||
<module name="dist_main" target="1.8" />
|
||||
<module name="dist_readmeFiles" target="1.8" />
|
||||
<module name="dist_startupScripts" target="1.8" />
|
||||
<module name="dist_test" target="1.8" />
|
||||
<module name="docs_main" target="1.8" />
|
||||
<module name="docs_source_example-code_integrationTest" target="1.8" />
|
||||
<module name="docs_source_example-code_main" target="1.8" />
|
||||
@ -98,6 +104,8 @@
|
||||
<module name="jfx_test" target="1.8" />
|
||||
<module name="kryo-hook_main" target="1.8" />
|
||||
<module name="kryo-hook_test" target="1.8" />
|
||||
<module name="launcher_main" target="1.8" />
|
||||
<module name="launcher_test" target="1.8" />
|
||||
<module name="loadtest_main" target="1.8" />
|
||||
<module name="loadtest_test" target="1.8" />
|
||||
<module name="mock_main" target="1.8" />
|
||||
|
@ -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
|
||||
|
19
launcher/build.gradle
Normal file
19
launcher/build.gradle
Normal file
@ -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'
|
||||
}
|
81
launcher/src/main/kotlin/net/corda/launcher/Launcher.kt
Normal file
81
launcher/src/main/kotlin/net/corda/launcher/Launcher.kt
Normal file
@ -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<String>) {
|
||||
|
||||
if(args.isEmpty()) {
|
||||
println("Usage: launcher <main-class-name> [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<String>::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>): String? {
|
||||
val idx = args.indexOf("--base-directory")
|
||||
return if (idx != -1 && idx < args.lastIndex) {
|
||||
args[idx + 1]
|
||||
} else null
|
||||
}
|
||||
|
||||
private fun Path.jarFiles(): List<Path> {
|
||||
return Files.newDirectoryStream(this).filter { it.toString().endsWith(".jar") }
|
||||
}
|
11
launcher/src/main/kotlin/net/corda/launcher/Loader.kt
Normal file
11
launcher/src/main/kotlin/net/corda/launcher/Loader.kt
Normal file
@ -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<URL>) {
|
||||
urls.forEach { addURL(it) }
|
||||
}
|
||||
}
|
59
launcher/src/main/kotlin/net/corda/launcher/Settings.kt
Normal file
59
launcher/src/main/kotlin/net/corda/launcher/Settings.kt
Normal file
@ -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<URL>
|
||||
|
||||
// Plugin directories (all contained jar files are added to classpath)
|
||||
val PLUGINS: List<Path>
|
||||
|
||||
// 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<URL> {
|
||||
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<Path> {
|
||||
val ext = config.getProperty("plugins")
|
||||
|
||||
return ext?.let {
|
||||
it.split(':').map { Paths.get(it) }
|
||||
} ?: emptyList()
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
@ -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 ""))
|
||||
}
|
||||
}
|
||||
|
||||
fun generateEmptyCrls(tempFolder: TemporaryFolder, rootCertAndKeyPair: CertificateAndKeyPair, directEndpoint: URL, indirectEndpoint: URL): Pair<Path, Path> {
|
||||
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() }
|
||||
}
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -23,14 +23,15 @@ fun createSignedCrl(issuerCertificate: X509Certificate,
|
||||
endpointUrl: URL,
|
||||
nextUpdateInterval: Duration,
|
||||
signer: Signer,
|
||||
includeInCrl: List<CertificateRevocationRequestData>): X509CRL {
|
||||
includeInCrl: List<CertificateRevocationRequestData>,
|
||||
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)
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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<String>) {
|
||||
|
||||
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<CertPat
|
||||
return Pair(csrCertPathAndKey, networkMapSigner)
|
||||
}
|
||||
|
||||
private fun rootKeyGenMode(cmdLineOptions: DoormanCmdLineOptions, config: NetworkManagementServerConfig) {
|
||||
generateRootKeyPair(
|
||||
private fun rootKeyGenMode(cmdLineOptions: DoormanCmdLineOptions, config: NetworkManagementServerConfig): CertificateAndKeyPair {
|
||||
return generateRootKeyPair(
|
||||
requireNotNull(config.rootStorePath) { "The 'rootStorePath' parameter must be specified when generating keys!" },
|
||||
config.rootKeystorePassword,
|
||||
config.rootPrivateKeyPassword,
|
||||
@ -77,7 +85,8 @@ private fun caKeyGenMode(config: NetworkManagementServerConfig) {
|
||||
config.rootKeystorePassword,
|
||||
config.rootPrivateKeyPassword,
|
||||
config.keystorePassword,
|
||||
config.caPrivateKeyPassword
|
||||
config.caPrivateKeyPassword,
|
||||
config.caCrlUrl
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -20,6 +20,7 @@ import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||
import org.apache.commons.io.FileUtils
|
||||
import java.io.Closeable
|
||||
import java.net.URI
|
||||
import java.time.Duration
|
||||
@ -94,9 +95,9 @@ class NetworkManagementServer(dataSourceProperties: Properties,
|
||||
val requestProcessor = if (jiraConfig != null) {
|
||||
val jiraWebAPI = AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(URI(jiraConfig.address), jiraConfig.username, jiraConfig.password)
|
||||
val jiraClient = CsrJiraClient(jiraWebAPI, jiraConfig.projectCode)
|
||||
JiraCsrHandler(jiraClient, csrStorage, DefaultCsrHandler(csrStorage, csrCertPathAndKey))
|
||||
JiraCsrHandler(jiraClient, csrStorage, DefaultCsrHandler(csrStorage, csrCertPathAndKey, config.crlEndpoint))
|
||||
} else {
|
||||
DefaultCsrHandler(csrStorage, csrCertPathAndKey)
|
||||
DefaultCsrHandler(csrStorage, csrCertPathAndKey, config.crlEndpoint)
|
||||
}
|
||||
|
||||
val scheduledExecutor = Executors.newScheduledThreadPool(1)
|
||||
@ -131,7 +132,7 @@ class NetworkManagementServer(dataSourceProperties: Properties,
|
||||
val crlHandler = csrCertPathAndKeyPair?.let {
|
||||
LocalCrlHandler(crrStorage,
|
||||
crlStorage,
|
||||
CertificateAndKeyPair(it.certPath.first(), it.toKeyPair()),
|
||||
CertificateAndKeyPair(it.certPath[0], it.toKeyPair()),
|
||||
Duration.ofMillis(config.localSigning!!.crlUpdateInterval),
|
||||
config.localSigning.crlEndpoint)
|
||||
}
|
||||
@ -158,7 +159,13 @@ class NetworkManagementServer(dataSourceProperties: Properties,
|
||||
scheduledExecutor.scheduleAtFixedRate(approvalThread, config.approveInterval, config.approveInterval, TimeUnit.MILLISECONDS)
|
||||
closeActions += scheduledExecutor::shutdown
|
||||
// TODO start socket server
|
||||
return Pair(CertificateRevocationRequestWebService(crrHandler), CertificateRevocationListWebService(crlStorage, Duration.ofMillis(config.crlCacheTimeout)))
|
||||
return Pair(
|
||||
CertificateRevocationRequestWebService(crrHandler),
|
||||
CertificateRevocationListWebService(
|
||||
crlStorage,
|
||||
FileUtils.readFileToByteArray(config.caCrlPath.toFile()),
|
||||
FileUtils.readFileToByteArray(config.emptyCrlPath.toFile()),
|
||||
Duration.ofMillis(config.crlCacheTimeout)))
|
||||
}
|
||||
|
||||
fun start(hostAndPort: NetworkHostAndPort,
|
||||
|
@ -11,10 +11,14 @@
|
||||
package com.r3.corda.networkmanage.doorman
|
||||
|
||||
import com.r3.corda.networkmanage.common.utils.CORDA_NETWORK_MAP
|
||||
import com.r3.corda.networkmanage.common.utils.createSignedCrl
|
||||
import com.r3.corda.networkmanage.doorman.signer.LocalSigner
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.SignatureScheme
|
||||
import net.corda.core.internal.createDirectories
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.utilities.days
|
||||
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||
import net.corda.nodeapi.internal.crypto.CertificateType
|
||||
import net.corda.nodeapi.internal.crypto.X509KeyStore
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_INTERMEDIATE_CA
|
||||
@ -22,6 +26,8 @@ import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities.createCertificate
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities.createSelfSignedCACertificate
|
||||
import org.apache.commons.io.FileUtils
|
||||
import java.net.URL
|
||||
import java.nio.file.Path
|
||||
import javax.security.auth.x500.X500Principal
|
||||
import kotlin.system.exitProcess
|
||||
@ -41,7 +47,7 @@ internal fun readPassword(fmt: String): String {
|
||||
}
|
||||
|
||||
// Keygen utilities.
|
||||
fun generateRootKeyPair(rootStoreFile: Path, rootKeystorePass: String?, rootPrivateKeyPass: String?, networkRootTrustPass: String?) {
|
||||
fun generateRootKeyPair(rootStoreFile: Path, rootKeystorePass: String?, rootPrivateKeyPass: String?, networkRootTrustPass: String?): CertificateAndKeyPair {
|
||||
println("Generating Root CA keypair and certificate.")
|
||||
// Get password from console if not in config.
|
||||
val rootKeystorePassword = rootKeystorePass ?: readPassword("Root Keystore Password: ")
|
||||
@ -76,9 +82,22 @@ fun generateRootKeyPair(rootStoreFile: Path, rootKeystorePass: String?, rootPriv
|
||||
println("Trust store for distribution to nodes created in $trustStorePath")
|
||||
println("Root CA keypair and certificate stored in ${rootStoreFile.toAbsolutePath()}.")
|
||||
println(rootCert)
|
||||
return CertificateAndKeyPair(rootCert, selfSignKey)
|
||||
}
|
||||
|
||||
fun generateSigningKeyPairs(keystoreFile: Path, rootStoreFile: Path, rootKeystorePass: String?, rootPrivateKeyPass: String?, keystorePass: String?, caPrivateKeyPass: String?) {
|
||||
fun createEmptyCrls(rootCertificateAndKeyPair: CertificateAndKeyPair, emptyCrlPath: Path, emptyCrlUrl: URL, caCrlPath: Path, caCrlUrl: URL) {
|
||||
val rootCert = rootCertificateAndKeyPair.certificate
|
||||
val rootKey = rootCertificateAndKeyPair.keyPair.private
|
||||
val emptyCrl = createSignedCrl(rootCert, emptyCrlUrl, 3650.days, LocalSigner(rootKey, rootCert), emptyList(), true)
|
||||
FileUtils.writeByteArrayToFile(emptyCrlPath.toFile(), emptyCrl.encoded)
|
||||
val caCrl = createSignedCrl(rootCert, caCrlUrl, 3650.days, LocalSigner(rootKey, rootCert), emptyList())
|
||||
FileUtils.writeByteArrayToFile(caCrlPath.toFile(), caCrl.encoded)
|
||||
println("Empty CRL: $emptyCrl")
|
||||
println("CA CRL: $caCrl")
|
||||
println("Root signed empty and CA CRL files created in $emptyCrlPath and $caCrlPath respectively")
|
||||
}
|
||||
|
||||
fun generateSigningKeyPairs(keystoreFile: Path, rootStoreFile: Path, rootKeystorePass: String?, rootPrivateKeyPass: String?, keystorePass: String?, caPrivateKeyPass: String?, caCrlUrl: URL?) {
|
||||
println("Generating intermediate and network map key pairs and certificates using root key store $rootStoreFile.")
|
||||
// Get password from console if not in config.
|
||||
val rootKeystorePassword = rootKeystorePass ?: readPassword("Root key store password: ")
|
||||
@ -106,7 +125,8 @@ fun generateSigningKeyPairs(keystoreFile: Path, rootStoreFile: Path, rootKeystor
|
||||
rootKeyPairAndCert.certificate,
|
||||
rootKeyPairAndCert.keyPair,
|
||||
subject,
|
||||
keyPair.public
|
||||
keyPair.public,
|
||||
crlDistPoint = caCrlUrl?.toString()
|
||||
)
|
||||
|
||||
keyStore.update {
|
||||
|
@ -11,18 +11,18 @@
|
||||
package com.r3.corda.networkmanage.doorman.signer
|
||||
|
||||
import com.r3.corda.networkmanage.common.persistence.CertificateRevocationListStorage
|
||||
import com.r3.corda.networkmanage.common.persistence.CertificateRevocationListStorage.Companion.DOORMAN_SIGNATURE
|
||||
import com.r3.corda.networkmanage.common.persistence.CertificateRevocationRequestStorage
|
||||
import com.r3.corda.networkmanage.common.persistence.CrlIssuer
|
||||
import com.r3.corda.networkmanage.common.persistence.RequestStatus
|
||||
import com.r3.corda.networkmanage.common.signer.CertificateRevocationListSigner
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||
import java.net.URL
|
||||
import java.time.Duration
|
||||
import com.r3.corda.networkmanage.common.persistence.CertificateRevocationListStorage.Companion.DOORMAN_SIGNATURE
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.trace
|
||||
|
||||
class LocalCrlHandler(private val crrStorage: CertificateRevocationRequestStorage,
|
||||
crlStorage: CertificateRevocationListStorage,
|
||||
private val crlStorage: CertificateRevocationListStorage,
|
||||
issuerCertAndKey: CertificateAndKeyPair,
|
||||
crlUpdateInterval: Duration,
|
||||
crlEndpoint: URL) {
|
||||
@ -38,19 +38,22 @@ class LocalCrlHandler(private val crrStorage: CertificateRevocationRequestStorag
|
||||
LocalSigner(issuerCertAndKey))
|
||||
|
||||
fun signCrl() {
|
||||
if (crlStorage.getCertificateRevocationList(CrlIssuer.DOORMAN) == null) {
|
||||
val crl = crlSigner.createSignedCRL(emptyList(), emptyList(), DOORMAN_SIGNATURE)
|
||||
logger.info("Saving a new empty CRL: $crl")
|
||||
return
|
||||
}
|
||||
logger.info("Executing CRL signing...")
|
||||
val approvedRequests = crrStorage.getRevocationRequests(RequestStatus.APPROVED)
|
||||
logger.debug("Approved certificate revocation requests retrieved.")
|
||||
logger.trace { approvedRequests.toString() }
|
||||
logger.debug("Approved certificate revocation requests retrieved: $approvedRequests")
|
||||
if (approvedRequests.isEmpty()) {
|
||||
// Nothing to add to the current CRL
|
||||
logger.debug("There are no APPROVED certificate revocation requests. Aborting CRL signing.")
|
||||
return
|
||||
}
|
||||
val currentRequests = crrStorage.getRevocationRequests(RequestStatus.DONE)
|
||||
logger.debug("Existing certificate revocation requests retrieved.")
|
||||
logger.trace { currentRequests.toString() }
|
||||
crlSigner.createSignedCRL(approvedRequests, currentRequests, DOORMAN_SIGNATURE)
|
||||
logger.info("New CRL signed.")
|
||||
logger.debug("Existing certificate revocation requests retrieved: $currentRequests")
|
||||
val crl = crlSigner.createSignedCRL(approvedRequests, currentRequests, DOORMAN_SIGNATURE)
|
||||
logger.info("New CRL signed: $crl")
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ import org.bouncycastle.asn1.x509.GeneralSubtree
|
||||
import org.bouncycastle.asn1.x509.NameConstraints
|
||||
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
||||
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
|
||||
import java.net.URL
|
||||
import java.security.cert.CertPath
|
||||
import javax.security.auth.x500.X500Principal
|
||||
|
||||
@ -34,7 +35,8 @@ interface CsrHandler {
|
||||
}
|
||||
|
||||
class DefaultCsrHandler(private val storage: CertificateSigningRequestStorage,
|
||||
private val csrCertPathAndKey: CertPathAndKey?) : CsrHandler {
|
||||
private val csrCertPathAndKey: CertPathAndKey?,
|
||||
private val crlDistributionPoint: URL? = null) : CsrHandler {
|
||||
|
||||
override fun processRequests() {
|
||||
if (csrCertPathAndKey == null) return
|
||||
@ -75,7 +77,8 @@ class DefaultCsrHandler(private val storage: CertificateSigningRequestStorage,
|
||||
csrCertPathAndKey.toKeyPair(),
|
||||
X500Principal(request.subject.encoded),
|
||||
request.publicKey,
|
||||
nameConstraints = nameConstraints)
|
||||
nameConstraints = nameConstraints,
|
||||
crlDistPoint = crlDistributionPoint?.toString())
|
||||
return X509CertificateFactory().generateCertPath(listOf(nodeCaCert) + csrCertPathAndKey.certPath)
|
||||
}
|
||||
}
|
||||
|
@ -59,10 +59,10 @@ class JiraCrrHandler(private val jiraClient: CrrJiraClient,
|
||||
|
||||
private fun updateJiraTickets(approvedRequest: List<ApprovedRequest>, rejectedRequest: List<RejectedRequest>) {
|
||||
// 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) }
|
||||
|
@ -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<CrlIssuer, ByteArray> = 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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)!!
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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'
|
||||
|
5
node/dist/README.md
vendored
Normal file
5
node/dist/README.md
vendored
Normal file
@ -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.
|
159
node/dist/build.gradle
vendored
Normal file
159
node/dist/build.gradle
vendored
Normal file
@ -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}")
|
||||
}
|
||||
}
|
||||
|
20
node/dist/src/main/resources/bin/corda
vendored
Executable file
20
node/dist/src/main/resources/bin/corda
vendored
Executable file
@ -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} "$@"
|
9
node/dist/src/main/resources/readme/README
vendored
Normal file
9
node/dist/src/main/resources/readme/README
vendored
Normal file
@ -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
|
61
node/dist/src/main/resources/readme/example-node.conf
vendored
Normal file
61
node/dist/src/main/resources/readme/example-node.conf
vendored
Normal file
@ -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
|
||||
}
|
@ -68,7 +68,12 @@ class NodeArgsParser : AbstractArgsParser<CmdLineOptions>() {
|
||||
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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
7
tools/notarytest/README.md
Normal file
7
tools/notarytest/README.md
Normal file
@ -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.
|
61
tools/notarytest/build.gradle
Normal file
61
tools/notarytest/build.gradle
Normal file
@ -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'
|
||||
)
|
||||
}
|
||||
}
|
@ -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<String>) = 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
|
||||
)
|
||||
}
|
||||
}
|
@ -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<String>) {
|
||||
// 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<Long> {
|
||||
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<Long>, 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()
|
||||
}
|
@ -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<T : AsyncCFTNotaryService>(
|
||||
private val serviceType: Class<T>,
|
||||
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<Long>() {
|
||||
private val keyPairGenerator = Generator.long().map { entropyToKeyPair(BigInteger.valueOf(it)) }
|
||||
private val publicKeyGeneratorSingle = Generator.pure(generateKeyPair().public)
|
||||
private val partyGenerator: Generator<Party> = 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<CordaFuture<AsyncUniquenessProvider.Result>>()
|
||||
|
||||
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
|
||||
}
|
||||
}
|
@ -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<T : TrustedAuthorityNotaryService>(
|
||||
private val serviceType: Class<T>,
|
||||
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<Long>() {
|
||||
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<Party> = 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
|
||||
}
|
||||
}
|
@ -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<Void?> = 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<MySQLConfiguration>()
|
||||
return MySQLUniquenessProvider(createMetricsRegistry(), services.clock, mysqlConfig)
|
||||
}
|
||||
}
|
||||
|
||||
@StartableByRPC
|
||||
class JDBCLoadTestFlow(transactionCount: Int,
|
||||
batchSize: Int,
|
||||
inputStateCount: Int?
|
||||
) : AsyncLoadTestFlow<JDBCNotaryService>(JDBCNotaryService::class.java, transactionCount, batchSize, inputStateCount)
|
Loading…
x
Reference in New Issue
Block a user