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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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:
.. literalinclude:: ../../network-management/generator.conf
Invoke doorman with ``-?`` for a full list of supported command-line arguments.
General configuration parameters
--------------------------------
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
```
## 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
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.

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
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.ConfigParseOptions
import joptsimple.OptionParser
import net.corda.nodeapi.internal.*
import net.corda.nodeapi.internal.config.parseAs
import java.io.File
import java.nio.file.Path
import java.nio.file.Paths
/**
* 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.
*/

View File

@ -10,6 +10,7 @@
package com.r3.corda.networkmanage.dev
import com.r3.corda.networkmanage.common.configuration.parseCommandLine
import com.r3.corda.networkmanage.doorman.CORDA_X500_BASE
import net.corda.core.crypto.Crypto
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.
*/
fun main(args: Array<String>) {
run(parseParameters(parseCommandLine(*args)?.configFile))
run(parseParameters(parseCommandLine(*args)))
}
fun run(configuration: GeneratorConfiguration) {

View File

@ -10,15 +10,11 @@
package com.r3.corda.networkmanage.hsm.generator
import com.r3.corda.networkmanage.common.utils.ShowHelpException
import com.typesafe.config.ConfigFactory
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.crypto.CertificateType
import java.nio.file.Path
import java.nio.file.Paths
/**
* 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 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.
*/

View File

@ -10,6 +10,7 @@
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.utils.mapCryptoServerException
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")
fun main(args: Array<String>) {
run(parseParameters(parseCommandLine(*args)?.configFile))
run(parseParameters(parseCommandLine(*args)))
}
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
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_PASS
import net.corda.nodeapi.internal.DEV_CA_TRUST_STORE_FILE
@ -31,7 +31,7 @@ class GeneratorConfigurationTest {
@Test
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(DEV_CA_KEY_STORE_FILE, config.keyStoreFileName)
assertEquals(DEV_CA_KEY_STORE_PASS, config.keyStorePass)

View File

@ -10,8 +10,10 @@
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.typesafe.config.ConfigException
import joptsimple.OptionException
import net.corda.nodeapi.internal.crypto.CertificateType
import org.assertj.core.api.Assertions
import org.junit.Test
@ -28,23 +30,23 @@ class GeneratorParametersTest {
@Test
fun `should fail when config file is missing`() {
val message = assertFailsWith<IllegalStateException> {
val message = assertFailsWith<OptionException> {
parseCommandLine("--config-file", "not-existing-file")
}.message
Assertions.assertThat(message).contains("Config file ")
Assertions.assertThat(message).contains("not-existing-file")
}
@Test
fun `should throw ShowHelpException when help option is passed on the command line`() {
assertFailsWith<ShowHelpException> {
parseCommandLine("-?")
parseCommandLine("-h")
}
}
@Test
fun `should fail when config is invalid`() {
assertFailsWith<ConfigException.Missing> {
parseParameters(parseCommandLine("--config-file", invalidConfigPath).configFile)
parseParameters(parseCommandLine("--config-file", invalidConfigPath))
}
}
@ -69,6 +71,6 @@ class GeneratorParametersTest {
}
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 reason: CRLReason,
val reporter: String) {
init {
require(certificateSerialNumber != null || csrRequestId != null || legalName != null) {
"At least one of the following needs to be specified: certificateSerialNumber, csrRequestId, legalName."
companion object {
fun validateOptional(certificateSerialNumber: BigInteger?, csrRequestId: String?, legalName: CordaX500Name?) {
require(certificateSerialNumber != null || csrRequestId != null || legalName != null) {
"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-hsm'
include 'network-management:capsule-hsm-cert-generator'
include 'network-management:capsule-crr-submission'
include 'network-management:registration-tool'
include 'tools:jmeter'
include 'tools:explorer'