Merge branch 'master' into kat-merge-20180517

This commit is contained in:
Katelyn Baker 2018-05-18 14:42:05 +01:00 committed by GitHub
commit 3778f029df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 1323 additions and 102 deletions

8
.idea/compiler.xml generated
View File

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

View File

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

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

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

View 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()
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
View 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
View 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
View 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} "$@"

View 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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View 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'
)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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