ENT-992: Introducing the CRR submission tool (#633)

* Introducing the CRR submission tool

* Addressing review comments

* Addressing review comments - round 2 - Redesign of the tool.

* Fixing messages
This commit is contained in:
Michal Kit
2018-04-03 08:33:22 +01:00
committed by GitHub
parent 9c07e67100
commit 625d0447aa
17 changed files with 302 additions and 77 deletions

View File

@ -0,0 +1,5 @@
Certificate Revocation Request Submission Tool
==============================================
The purpose of the Certificate Revocation Request (CRR) Submission Tool is to facilitate the process of creating a CRR.
The tool is designed with the support line in mind, and assumes it is for internal (i.e. within the doorman service managing company) usage.

View File

@ -0,0 +1,12 @@
Running the Certificate Revocation Request Submission Tool
==========================================================
The purpose of this tool is to facilitate the certificate revocation request submission process.
See :doc:`crr-submission-tool` for more details.
See the Readme under ``network-management`` for detailed building instructions.
Command line argument
----------------------
At startup, the Certificate Revocation Request Submission Tool takes only one command line argument: ``--submission-url``,
that should be followed by the url to the certificate revocation request submission endpoint.

View File

@ -15,9 +15,6 @@ At startup, the HSM Certificate Generation Tool reads a configuration file, pass
This is an example of what a tool configuration file might look like: This is an example of what a tool configuration file might look like:
.. literalinclude:: ../../network-management/generator.conf .. literalinclude:: ../../network-management/generator.conf
Invoke doorman with ``-?`` for a full list of supported command-line arguments.
General configuration parameters General configuration parameters
-------------------------------- --------------------------------
Allowed parameters are: Allowed parameters are:

View File

@ -48,6 +48,19 @@ The built file will appear in
network-management/capsule-hsm-cert-generator/build/libs/hsm-cert-generator-<VERSION>.jar network-management/capsule-hsm-cert-generator/build/libs/hsm-cert-generator-<VERSION>.jar
``` ```
## Certificate Revocation Request Submission Tool
To build a fat jar containing all the CRR submission tool code you can simply invoke
```
./gradlew network-management:capsule-crr-submission:buildCrrSubmissionJAR
```
The built file will appear in
```
network-management/capsule-crr-submission/build/libs/crr-submission-<VERSION>.jar
```
# Logs # Logs
In order to set the desired logging level the system properties need to be used. In order to set the desired logging level the system properties need to be used.
Appropriate system properties can be set at the execution time. Appropriate system properties can be set at the execution time.

View File

@ -0,0 +1,49 @@
/*
* R3 Proprietary and Confidential
*
* Copyright (c) 2018 R3 Limited. All rights reserved.
*
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
*
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
*/
apply plugin: 'net.corda.plugins.publish-utils'
apply plugin: 'us.kirchmeier.capsule'
description 'HSM Certificate Generator'
version project(':network-management').version
configurations {
runtimeArtifacts.extendsFrom runtime
}
task buildCrrSubmissionJAR(type: FatCapsule, dependsOn: 'jar') {
applicationClass 'com.r3.corda.networkmanage.tools.crr.submission.MainKt'
archiveName "crr-submission-${version}.jar"
capsuleManifest {
applicationVersion = corda_release_version
systemProperties['visualvm.display.name'] = 'CRR Submission Tool'
minJavaVersion = '1.8.0'
jvmArgs = ['-XX:+UseG1GC']
}
applicationSource = files(
project(':network-management').configurations.runtime,
project(':network-management').jar
)
}
artifacts {
runtimeArtifacts buildCrrSubmissionJAR
publish buildCrrSubmissionJAR
}
jar {
classifier "ignore"
}
publish {
name 'crr-submission'
disableDefaultJar = true
}

View File

@ -0,0 +1,28 @@
package com.r3.corda.networkmanage.common.configuration
import com.r3.corda.networkmanage.common.utils.ShowHelpException
import joptsimple.OptionParser
import joptsimple.util.PathConverter
import joptsimple.util.PathProperties
import java.nio.file.Path
/**
* Parses key generator command line options.
*/
fun parseCommandLine(vararg args: String): Path {
val optionParser = OptionParser()
val configFileArg = optionParser
.accepts("config-file", "The path to the config file")
.withRequiredArg()
.required()
.describedAs("filepath")
.withValuesConvertedBy(PathConverter(PathProperties.FILE_EXISTING))
val helpOption = optionParser.acceptsAll(listOf("h", "help"), "show help").forHelp()
val optionSet = optionParser.parse(*args)
// Print help and exit on help option or if there are missing options.
if (optionSet.has(helpOption) || !optionSet.has(configFileArg)) {
throw ShowHelpException(optionParser)
}
return optionSet.valueOf(configFileArg).toAbsolutePath()
}

View File

@ -10,16 +10,12 @@
package com.r3.corda.networkmanage.dev package com.r3.corda.networkmanage.dev
import com.r3.corda.networkmanage.common.utils.ShowHelpException
import com.r3.corda.networkmanage.hsm.generator.CommandLineOptions
import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigParseOptions import com.typesafe.config.ConfigParseOptions
import joptsimple.OptionParser
import net.corda.nodeapi.internal.* import net.corda.nodeapi.internal.*
import net.corda.nodeapi.internal.config.parseAs import net.corda.nodeapi.internal.config.parseAs
import java.io.File import java.io.File
import java.nio.file.Path import java.nio.file.Path
import java.nio.file.Paths
/** /**
* Holds configuration necessary for generating DEV key store and trust store. * Holds configuration necessary for generating DEV key store and trust store.
@ -35,29 +31,6 @@ data class GeneratorConfiguration(val privateKeyPass: String = DEV_CA_PRIVATE_KE
} }
} }
/**
* Parses dev generator command line options.
*/
fun parseCommandLine(vararg args: String): CommandLineOptions? {
val optionParser = OptionParser()
val configFileArg = optionParser
.accepts("config-file", "The path to the config file")
.withRequiredArg()
.describedAs("filepath")
val helpOption = optionParser.acceptsAll(listOf("h", "?", "help"), "show help").forHelp()
val optionSet = optionParser.parse(*args)
// Print help and exit on help option.
if (optionSet.has(helpOption)) {
throw ShowHelpException(optionParser)
}
return if (optionSet.has(configFileArg)) {
CommandLineOptions(Paths.get(optionSet.valueOf(configFileArg)).toAbsolutePath())
} else {
null
}
}
/** /**
* Parses a configuration file, which contains all the configuration - i.e. for the key store generator. * Parses a configuration file, which contains all the configuration - i.e. for the key store generator.
*/ */

View File

@ -10,6 +10,7 @@
package com.r3.corda.networkmanage.dev package com.r3.corda.networkmanage.dev
import com.r3.corda.networkmanage.common.configuration.parseCommandLine
import com.r3.corda.networkmanage.doorman.CORDA_X500_BASE import com.r3.corda.networkmanage.doorman.CORDA_X500_BASE
import net.corda.core.crypto.Crypto import net.corda.core.crypto.Crypto
import net.corda.core.internal.createDirectories import net.corda.core.internal.createDirectories
@ -33,7 +34,7 @@ private val logger = LogManager.getLogger("com.r3.corda.networkmanage.dev.Main")
* Look for the 'certificates' directory. * Look for the 'certificates' directory.
*/ */
fun main(args: Array<String>) { fun main(args: Array<String>) {
run(parseParameters(parseCommandLine(*args)?.configFile)) run(parseParameters(parseCommandLine(*args)))
} }
fun run(configuration: GeneratorConfiguration) { fun run(configuration: GeneratorConfiguration) {

View File

@ -10,15 +10,11 @@
package com.r3.corda.networkmanage.hsm.generator package com.r3.corda.networkmanage.hsm.generator
import com.r3.corda.networkmanage.common.utils.ShowHelpException
import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigParseOptions import com.typesafe.config.ConfigParseOptions
import joptsimple.OptionParser
import net.corda.core.internal.isRegularFile
import net.corda.nodeapi.internal.config.parseAs import net.corda.nodeapi.internal.config.parseAs
import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.CertificateType
import java.nio.file.Path import java.nio.file.Path
import java.nio.file.Paths
/** /**
* Holds configuration necessary for user's authentication against HSM. * Holds configuration necessary for user's authentication against HSM.
@ -62,37 +58,6 @@ data class CertificateConfiguration(val keyGroup: String,
val keyCurve: String, // we use "NIST-P256", check Utimaco docs for other options val keyCurve: String, // we use "NIST-P256", check Utimaco docs for other options
val keyGenMechanism: Int) // MECH_KEYGEN_UNCOMP = 4 or MECH_RND_REAL = 0 val keyGenMechanism: Int) // MECH_KEYGEN_UNCOMP = 4 or MECH_RND_REAL = 0
/**
* Holds arguments for command line options.
*/
data class CommandLineOptions(val configFile: Path) {
init {
check(configFile.isRegularFile()) { "Config file $configFile does not exist" }
}
}
/**
* Parses key generator command line options.
*/
fun parseCommandLine(vararg args: String): CommandLineOptions {
val optionParser = OptionParser()
val configFileArg = optionParser
.accepts("config-file", "The path to the config file")
.withRequiredArg()
.describedAs("filepath")
val helpOption = optionParser.acceptsAll(listOf("h", "?", "help"), "show help").forHelp()
val optionSet = optionParser.parse(*args)
// Print help and exit on help option or if there are missing options.
if (optionSet.has(helpOption) || !optionSet.has(configFileArg)) {
throw ShowHelpException(optionParser)
}
val configFile = Paths.get(optionSet.valueOf(configFileArg)).toAbsolutePath()
return CommandLineOptions(configFile)
}
/** /**
* Parses a configuration file, which contains all the configuration - i.e. for user and certificate parameters. * Parses a configuration file, which contains all the configuration - i.e. for user and certificate parameters.
*/ */

View File

@ -10,6 +10,7 @@
package com.r3.corda.networkmanage.hsm.generator package com.r3.corda.networkmanage.hsm.generator
import com.r3.corda.networkmanage.common.configuration.parseCommandLine
import com.r3.corda.networkmanage.hsm.authentication.CryptoServerProviderConfig import com.r3.corda.networkmanage.hsm.authentication.CryptoServerProviderConfig
import com.r3.corda.networkmanage.hsm.utils.mapCryptoServerException import com.r3.corda.networkmanage.hsm.utils.mapCryptoServerException
import net.corda.nodeapi.internal.crypto.CertificateType.ROOT_CA import net.corda.nodeapi.internal.crypto.CertificateType.ROOT_CA
@ -18,7 +19,7 @@ import org.apache.logging.log4j.LogManager
private val logger = LogManager.getLogger("com.r3.corda.networkmanage.hsm.generator.Main") private val logger = LogManager.getLogger("com.r3.corda.networkmanage.hsm.generator.Main")
fun main(args: Array<String>) { fun main(args: Array<String>) {
run(parseParameters(parseCommandLine(*args)?.configFile)) run(parseParameters(parseCommandLine(*args)))
} }
fun run(parameters: GeneratorParameters) { fun run(parameters: GeneratorParameters) {

View File

@ -0,0 +1,21 @@
package com.r3.corda.networkmanage.tools.crr.submission
import com.r3.corda.networkmanage.common.utils.ShowHelpException
import joptsimple.OptionParser
import java.net.URL
fun parseSubmissionUrl(vararg args: String): URL {
val optionParser = OptionParser()
val submissionUrlArg = optionParser
.accepts("submission-url", "CRR submission endpoint.")
.withRequiredArg()
.required()
val helpOption = optionParser.acceptsAll(listOf("h", "help"), "show help").forHelp()
val optionSet = optionParser.parse(*args)
// Print help and exit on help option or if there are missing options.
if (optionSet.has(helpOption) || !optionSet.has(submissionUrlArg)) {
throw ShowHelpException(optionParser)
}
return URL(optionSet.valueOf(submissionUrlArg))
}

View File

@ -0,0 +1,56 @@
package com.r3.corda.networkmanage.tools.crr.submission
import com.r3.corda.networkmanage.common.utils.initialiseSerialization
import com.r3.corda.networkmanage.hsm.authentication.ConsoleInputReader
import com.r3.corda.networkmanage.hsm.authentication.InputReader
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.post
import net.corda.core.serialization.serialize
import net.corda.nodeapi.internal.network.CertificateRevocationRequest
import org.apache.logging.log4j.LogManager
import java.math.BigInteger
import java.net.URL
import java.security.cert.CRLReason
private val logger = LogManager.getLogger("com.r3.corda.networkmanage.common.tools.crr.Main")
fun main(args: Array<String>) {
initialiseSerialization()
try {
submit(parseSubmissionUrl(*args))
} catch (e: Exception) {
logger.error("Error when submitting a certificate revocation request.", e)
throw e
}
}
fun submit(url: URL, inputReader: InputReader = ConsoleInputReader()) {
val certificateSerialNumber = inputReader.getOptionalInput("certificate serial number")?.let { BigInteger(it) }
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 reporter = inputReader.getRequiredInput("reporter of the revocation request")
val request = CertificateRevocationRequest(certificateSerialNumber, csrRequestId, legalName, reason, reporter)
logger.debug("POST to $url request: $request")
val requestId = String(url.post(request.serialize()))
logger.debug("Certificate revocation request successfully submitted. Request ID: $requestId")
println("Successfully submitted certificate revocation request. Generated request ID: $requestId")
}
private fun InputReader.getOptionalInput(attributeName: String): String? {
print("Type in $attributeName (press enter if not available):")
return this.readLine()?.let {
if (it.isBlank()) null else it
}
}
private fun InputReader.getRequiredInput(attributeName: String): String {
print("Type in $attributeName:")
val line = this.readLine()
return if (line == null || line.isNullOrBlank()) {
throw IllegalArgumentException("The $attributeName needs to be specified.")
} else {
line
}
}

View File

@ -10,7 +10,7 @@
package com.r3.corda.networkmanage.dev package com.r3.corda.networkmanage.dev
import com.r3.corda.networkmanage.hsm.generator.parseCommandLine import com.r3.corda.networkmanage.common.configuration.parseCommandLine
import net.corda.nodeapi.internal.DEV_CA_KEY_STORE_FILE import net.corda.nodeapi.internal.DEV_CA_KEY_STORE_FILE
import net.corda.nodeapi.internal.DEV_CA_KEY_STORE_PASS import net.corda.nodeapi.internal.DEV_CA_KEY_STORE_PASS
import net.corda.nodeapi.internal.DEV_CA_TRUST_STORE_FILE import net.corda.nodeapi.internal.DEV_CA_TRUST_STORE_FILE
@ -31,7 +31,7 @@ class GeneratorConfigurationTest {
@Test @Test
fun `config file is parsed correctly`() { fun `config file is parsed correctly`() {
val config = parseParameters(parseCommandLine("--config-file", configPath).configFile) val config = parseParameters(parseCommandLine("--config-file", configPath))
assertEquals(GeneratorConfiguration.DEFAULT_DIRECTORY, config.directory) assertEquals(GeneratorConfiguration.DEFAULT_DIRECTORY, config.directory)
assertEquals(DEV_CA_KEY_STORE_FILE, config.keyStoreFileName) assertEquals(DEV_CA_KEY_STORE_FILE, config.keyStoreFileName)
assertEquals(DEV_CA_KEY_STORE_PASS, config.keyStorePass) assertEquals(DEV_CA_KEY_STORE_PASS, config.keyStorePass)

View File

@ -10,8 +10,10 @@
package com.r3.corda.networkmanage.hsm.generator package com.r3.corda.networkmanage.hsm.generator
import com.r3.corda.networkmanage.common.configuration.parseCommandLine
import com.r3.corda.networkmanage.common.utils.ShowHelpException import com.r3.corda.networkmanage.common.utils.ShowHelpException
import com.typesafe.config.ConfigException import com.typesafe.config.ConfigException
import joptsimple.OptionException
import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.CertificateType
import org.assertj.core.api.Assertions import org.assertj.core.api.Assertions
import org.junit.Test import org.junit.Test
@ -28,23 +30,23 @@ class GeneratorParametersTest {
@Test @Test
fun `should fail when config file is missing`() { fun `should fail when config file is missing`() {
val message = assertFailsWith<IllegalStateException> { val message = assertFailsWith<OptionException> {
parseCommandLine("--config-file", "not-existing-file") parseCommandLine("--config-file", "not-existing-file")
}.message }.message
Assertions.assertThat(message).contains("Config file ") Assertions.assertThat(message).contains("not-existing-file")
} }
@Test @Test
fun `should throw ShowHelpException when help option is passed on the command line`() { fun `should throw ShowHelpException when help option is passed on the command line`() {
assertFailsWith<ShowHelpException> { assertFailsWith<ShowHelpException> {
parseCommandLine("-?") parseCommandLine("-h")
} }
} }
@Test @Test
fun `should fail when config is invalid`() { fun `should fail when config is invalid`() {
assertFailsWith<ConfigException.Missing> { assertFailsWith<ConfigException.Missing> {
parseParameters(parseCommandLine("--config-file", invalidConfigPath).configFile) parseParameters(parseCommandLine("--config-file", invalidConfigPath))
} }
} }
@ -69,6 +71,6 @@ class GeneratorParametersTest {
} }
private fun parseCommandLineAndGetParameters(): GeneratorParameters { private fun parseCommandLineAndGetParameters(): GeneratorParameters {
return parseParameters(parseCommandLine(*validArgs).configFile) return parseParameters(parseCommandLine(*validArgs))
} }
} }

View File

@ -0,0 +1,95 @@
/*
* R3 Proprietary and Confidential
*
* Copyright (c) 2018 R3 Limited. All rights reserved.
*
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
*
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
*/
package com.r3.corda.networkmanage.tools.crr.submission
import com.nhaarman.mockito_kotlin.eq
import com.nhaarman.mockito_kotlin.mock
import com.nhaarman.mockito_kotlin.verify
import com.nhaarman.mockito_kotlin.whenever
import com.r3.corda.networkmanage.doorman.NetworkManagementWebServer
import com.r3.corda.networkmanage.doorman.signer.CrrHandler
import com.r3.corda.networkmanage.doorman.webservice.CertificateRevocationRequestWebService
import com.r3.corda.networkmanage.hsm.authentication.InputReader
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.CordaX500Name
import net.corda.nodeapi.internal.network.CertificateRevocationRequest
import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.core.freeLocalHostAndPort
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import java.math.BigInteger
import java.net.URL
import java.security.cert.CRLReason
class CertificateRevocationRequestSubmissionToolTest {
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule(true)
private val hostAndPort = freeLocalHostAndPort()
private lateinit var webServer: NetworkManagementWebServer
private lateinit var inputReader: InputReader
@Before
fun setUp() {
inputReader = mock()
}
@After
fun close() {
webServer.close()
}
@Test
fun `submit request succeeds`() {
// given
val request = CertificateRevocationRequest(
certificateSerialNumber = BigInteger.TEN,
csrRequestId = "TestCSRId",
legalName = CordaX500Name.parse("O=TestOrg, C=GB, L=London"),
reason = CRLReason.KEY_COMPROMISE,
reporter = "TestReporter"
)
givenUserConsoleSequentialInputOnReadLine(request.certificateSerialNumber.toString(),
request.csrRequestId!!,
request.legalName.toString(),
request.reason.name,
request.reporter)
val requestId = SecureHash.randomSHA256().toString()
val requestProcessor = mock<CrrHandler> {
on { saveRevocationRequest(eq(request)) }.then { requestId }
}
startSigningServer(requestProcessor)
// when
submit(URL("http://$hostAndPort/certificate-revocation-request"), inputReader)
// then
verify(requestProcessor).saveRevocationRequest(eq(request))
}
private fun givenUserConsoleSequentialInputOnReadLine(vararg inputs: String) {
var sequence = whenever(inputReader.readLine()).thenReturn(inputs.first())
inputs.drop(1).forEach {
sequence = sequence.thenReturn(it)
}
}
private fun startSigningServer(handler: CrrHandler) {
webServer = NetworkManagementWebServer(hostAndPort, CertificateRevocationRequestWebService(handler))
webServer.start()
}
}

View File

@ -15,9 +15,15 @@ data class CertificateRevocationRequest(val certificateSerialNumber: BigInteger?
val legalName: CordaX500Name? = null, val legalName: CordaX500Name? = null,
val reason: CRLReason, val reason: CRLReason,
val reporter: String) { val reporter: String) {
init { companion object {
fun validateOptional(certificateSerialNumber: BigInteger?, csrRequestId: String?, legalName: CordaX500Name?) {
require(certificateSerialNumber != null || csrRequestId != null || legalName != null) { require(certificateSerialNumber != null || csrRequestId != null || legalName != null) {
"At least one of the following needs to be specified: certificateSerialNumber, csrRequestId, legalName." "At least one of the following needs to be specified: certificateSerialNumber, csrRequestId, legalName."
} }
} }
} }
init {
validateOptional(certificateSerialNumber, csrRequestId, legalName)
}
}

View File

@ -47,6 +47,7 @@ include 'network-management'
include 'network-management:capsule' include 'network-management:capsule'
include 'network-management:capsule-hsm' include 'network-management:capsule-hsm'
include 'network-management:capsule-hsm-cert-generator' include 'network-management:capsule-hsm-cert-generator'
include 'network-management:capsule-crr-submission'
include 'network-management:registration-tool' include 'network-management:registration-tool'
include 'tools:jmeter' include 'tools:jmeter'
include 'tools:explorer' include 'tools:explorer'