Adding a new service for CSR signing (#49)

* Adding a new service for CSR signing

* Adding a new service for CSR signing

* Removing rejection option

* Adding CSR log and removing rejection option

* Addressing review comments
This commit is contained in:
mkit 2017-10-09 11:34:42 +01:00 committed by GitHub
parent e393fdd292
commit e22570a81d
29 changed files with 6259 additions and 1 deletions

View File

@ -5,7 +5,7 @@ import net.corda.node.utilities.CordaPersistence
/**
* This storage automatically approves all created requests.
*/
class ApprovingAllCertificateRequestStorage(private val database: CordaPersistence) : DBCertificateRequestStorage(database) {
class ApprovingAllCertificateRequestStorage(database: CordaPersistence) : DBCertificateRequestStorage(database) {
override fun saveRequest(certificationData: CertificationRequestData): String {
val requestId = super.saveRequest(certificationData)
approveRequest(requestId)

View File

@ -45,6 +45,7 @@ open class DBCertificateRequestStorage(private val database: CordaPersistence) :
var approvedBy: String? = null,
@Column
@Enumerated(EnumType.STRING)
var status: Status = Status.New,
@Column(name = "signed_by", length = 512)

View File

@ -45,3 +45,5 @@ include 'cordform-common'
include 'doorman'
include 'verify-enclave'
include 'sgx-jvm/hsm-tool'
include 'signing-server'

View File

@ -0,0 +1,88 @@
ext {
// We use Corda release artifact dependencies instead of project dependencies to make sure each doorman releases are
// align with the corresponding Corda release.
corda_dependency_version = '0.16-20170913.101300-6'
}
version "$corda_dependency_version"
apply plugin: 'us.kirchmeier.capsule'
apply plugin: 'kotlin'
repositories {
mavenLocal()
mavenCentral()
maven {
url 'http://oss.sonatype.org/content/repositories/snapshots'
}
jcenter()
maven {
url 'http://ci-artifactory.corda.r3cev.com/artifactory/corda-dev'
}
}
configurations{
integrationTestCompile.extendsFrom testCompile
integrationTestRuntime.extendsFrom testRuntime
}
sourceSets{
integrationTest {
kotlin {
compileClasspath += main.output + test.output
runtimeClasspath += main.output + test.output
srcDir file('src/integration-test/kotlin')
}
java {
compileClasspath += main.output + test.output
runtimeClasspath += main.output + test.output
srcDir file('src/integration-test/java')
}
resources {
srcDir file('src/integration-test/resources')
}
}
}
task buildSigningServerJAR(type: FatCapsule, dependsOn: 'jar') {
group = 'build'
applicationClass 'com.r3.corda.signing.MainKt'
capsuleManifest {
applicationVersion = corda_dependency_version
systemProperties['visualvm.display.name'] = 'Signing Server'
minJavaVersion = '1.8.0'
jvmArgs = ['-XX:+UseG1GC']
}
// Make the resulting JAR file directly executable on UNIX by prepending a shell script to it.
// This lets you run the file like so: ./corda.jar
// Other than being slightly less typing, this has one big advantage: Ctrl-C works properly in the terminal.
reallyExecutable { trampolining() }
}
dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
compile fileTree(dir: 'libs', include: '*.jar')
compile "net.corda:corda-core:$corda_dependency_version"
compile "net.corda:corda-node:$corda_dependency_version"
compile "net.corda:corda-node-api:$corda_dependency_version"
testCompile "net.corda:corda-test-utils:$corda_dependency_version"
testCompile "net.corda:corda-node-driver:$corda_dependency_version"
// Log4J: logging framework (with SLF4J bindings)
compile "org.apache.logging.log4j:log4j-slf4j-impl:${log4j_version}"
compile "org.apache.logging.log4j:log4j-core:${log4j_version}"
compile "org.apache.logging.log4j:log4j-web:${log4j_version}"
// JOpt: for command line flags.
compile "net.sf.jopt-simple:jopt-simple:5.0.2"
// TypeSafe Config: for simple and human friendly config files.
compile "com.typesafe:config:1.3.0"
// Unit testing helpers.
testCompile 'junit:junit:4.12'
testCompile "org.assertj:assertj-core:${assertj_version}"
testCompile project(':doorman')
}

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,38 @@
package net.corda.signing
import net.corda.signing.configuration.Parameters
import java.util.*
import net.corda.signing.SigningServiceIntegrationTest.Companion.DB_NAME
import net.corda.signing.SigningServiceIntegrationTest.Companion.HOST
import net.corda.signing.SigningServiceIntegrationTest.Companion.H2_TCP_PORT
/**
* The main method for an interactive HSM signing service test/demo. It is supposed to be executed with the
* `DEMO - Create CSR and poll` method located in the [SigningServiceIntegrationTest], which is responsible for simulating
* CSR creation on the Doorman side.
* Execution instructions:
* 1) It is assumed that the HSM simulator is installed locally (or via means of the VM) and accessible under the address
* configured under the 'device' parameter (defaults to 3001@127.0.0.1). If that is not the case please specify
* a correct 'device' parameter value. Also, it is assumed that the HSM setup consists of a cryptographic user eligible to
* sign the CSRs (and potentially to generate new root and intermediate certificates).
* 2) Run the `DEMO - Create CSR and poll` as a regular test from your IntelliJ.
* The method starts the doorman, creates 3 CSRs for ALICE, BOB and CHARLIE
* and then polls the doorman until all 3 requests are signed.
* 3) Once the `DEMO - Create CSR and poll` is started, execute the following main method
* and interact with console menu options presented.
*/
fun main(args: Array<String>) {
run(Parameters(
dataSourceProperties = makeTestDataSourceProperties("localhost"),
databaseProperties = makeNotInitialisingTestDatabaseProperties()
))
}
private fun makeTestDataSourceProperties(nodeName: String): Properties {
val props = Properties()
props.setProperty("dataSourceClassName", "org.h2.jdbcx.JdbcDataSource")
props.setProperty("dataSource.url", "jdbc:h2:tcp://$HOST:$H2_TCP_PORT/mem:$DB_NAME;DB_CLOSE_DELAY=-1")
props.setProperty("dataSource.user", "sa")
props.setProperty("dataSource.password", "")
return props
}

View File

@ -0,0 +1,188 @@
package net.corda.signing
import com.google.common.net.HostAndPort
import com.nhaarman.mockito_kotlin.any
import com.nhaarman.mockito_kotlin.mock
import com.nhaarman.mockito_kotlin.verify
import com.nhaarman.mockito_kotlin.whenever
import com.r3.corda.doorman.DoormanServer
import com.r3.corda.doorman.buildCertPath
import com.r3.corda.doorman.persistence.ApprovingAllCertificateRequestStorage
import com.r3.corda.doorman.persistence.DoormanSchemaService
import com.r3.corda.doorman.signer.DefaultCsrHandler
import com.r3.corda.doorman.signer.ExternalSigner
import com.r3.corda.doorman.toX509Certificate
import net.corda.core.crypto.Crypto
import net.corda.core.identity.CordaX500Name
import net.corda.core.utilities.seconds
import net.corda.node.utilities.CertificateType
import net.corda.node.utilities.X509Utilities
import net.corda.node.utilities.configureDatabase
import net.corda.node.utilities.registration.HTTPNetworkRegistrationService
import net.corda.node.utilities.registration.NetworkRegistrationHelper
import net.corda.signing.hsm.HsmSigner
import net.corda.signing.persistence.ApprovedCertificateRequestData
import net.corda.signing.persistence.DBCertificateRequestStorage
import net.corda.signing.persistence.SigningServerSchemaService
import net.corda.testing.ALICE
import net.corda.testing.BOB
import net.corda.testing.CHARLIE
import net.corda.testing.testNodeConfiguration
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
import org.h2.tools.Server
import org.junit.*
import org.junit.rules.TemporaryFolder
import java.net.URL
import java.util.*
import kotlin.concurrent.scheduleAtFixedRate
import kotlin.concurrent.thread
import com.r3.corda.doorman.persistence.DBCertificateRequestStorage.CertificateSigningRequest as DoormanRequest
import net.corda.signing.persistence.DBCertificateRequestStorage.CertificateSigningRequest as SigningServerRequest
class SigningServiceIntegrationTest {
companion object {
val H2_TCP_PORT = "8092"
val HOST = "localhost"
val DB_NAME = "test_db"
}
@Rule
@JvmField
val tempFolder = TemporaryFolder()
private lateinit var timer: Timer
@Before
fun setUp() {
timer = Timer()
}
@After
fun tearDown() {
timer.cancel()
}
private fun givenSignerSigningAllRequests(storage: DBCertificateRequestStorage): HsmSigner {
// Create all certificates
val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Integration Test Corda Node Root CA",
organisation = "R3 Ltd", locality = "London", country = "GB").x500Name, rootCAKey)
val intermediateCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey,
CordaX500Name(commonName = "Integration Test Corda Node Intermediate CA", locality = "London", country = "GB",
organisation = "R3 Ltd"), intermediateCAKey.public)
// Mock signing logic but keep certificate persistence
return mock {
on { sign(any()) }.then {
@Suppress("UNCHECKED_CAST")
val toSign = it.arguments[0] as List<ApprovedCertificateRequestData>
toSign.forEach {
JcaPKCS10CertificationRequest(it.request).run {
val certificate = X509Utilities.createCertificate(CertificateType.TLS, intermediateCACert, intermediateCAKey, subject, publicKey).toX509Certificate()
it.certPath = buildCertPath(certificate, rootCACert.toX509Certificate())
}
}
storage.sign(toSign, listOf("TEST"))
}
}
}
@Test
fun `Signing service communicates with Doorman`() {
//Start doorman server
val doormanStorage = ApprovingAllCertificateRequestStorage(configureDatabase(makeTestDataSourceProperties(), null, { DoormanSchemaService() }, createIdentityService = {
// Identity service not needed doorman, corda persistence is not very generic.
throw UnsupportedOperationException()
}))
val doorman = DoormanServer(HostAndPort.fromParts(HOST, 0), DefaultCsrHandler(doormanStorage, ExternalSigner()))
doorman.start()
// Start Corda network registration.
val config = testNodeConfiguration(
baseDirectory = tempFolder.root.toPath(),
myLegalName = ALICE.name).also {
whenever(it.certificateSigningService).thenReturn(URL("http://$HOST:${doorman.hostAndPort.port}"))
}
val signingServiceStorage = DBCertificateRequestStorage(configureDatabase(makeTestDataSourceProperties(), makeNotInitialisingTestDatabaseProperties(), { SigningServerSchemaService() }, createIdentityService = {
// Identity service not needed doorman, corda persistence is not very generic.
throw UnsupportedOperationException()
}))
val hsmSigner = givenSignerSigningAllRequests(signingServiceStorage)
// Poll the database for approved requests
timer.scheduleAtFixedRate(2.seconds.toMillis(), 1.seconds.toMillis()) {
// The purpose of this tests is to validate the communication between this service and Doorman
// by the means of data in the shared database.
// Therefore the HSM interaction logic is mocked here.
val approved = signingServiceStorage.getApprovedRequests()
if (approved.isNotEmpty()) {
hsmSigner.sign(approved)
timer.cancel()
}
}
NetworkRegistrationHelper(config, HTTPNetworkRegistrationService(config.certificateSigningService)).buildKeystore()
verify(hsmSigner).sign(any())
doorman.close()
}
/*
* Piece of code is purely for demo purposes and should not be considered as actual test (therefore it is ignored).
* Its purpose is to produce 3 CSRs and wait (polling Doorman) for external signature.
* The use of the jUnit testing framework was chosen due to the convenience reasons: mocking, tempFolder storage.
* It is meant to be run together with the [DemoMain.main] method, which executes HSM signing service.
* The split is done due to the limited console support while executing tests and inability to capture user's input there.
*
*/
@Test
@Ignore
fun `DEMO - Create CSR and poll`() {
//Start doorman server
val doormanStorage = ApprovingAllCertificateRequestStorage(configureDatabase(makeTestDataSourceProperties(), null, { DoormanSchemaService() }, createIdentityService = {
// Identity service not needed doorman, corda persistence is not very generic.
throw UnsupportedOperationException()
}))
val doorman = DoormanServer(HostAndPort.fromParts(HOST, 0), DefaultCsrHandler(doormanStorage, ExternalSigner()))
doorman.start()
thread(start = true, isDaemon = true) {
val h2ServerArgs = arrayOf("-tcpPort", H2_TCP_PORT, "-tcpAllowOthers")
Server.createTcpServer(*h2ServerArgs).start()
}
// Start Corda network registration.
(1..3).map {
thread(start = true) {
val config = testNodeConfiguration(
baseDirectory = tempFolder.root.toPath(),
myLegalName = when(it) {
1 -> ALICE.name
2 -> BOB.name
3 -> CHARLIE.name
else -> throw IllegalArgumentException("Unrecognised option")
}).also {
whenever(it.certificateSigningService).thenReturn(URL("http://$HOST:${doorman.hostAndPort.port}"))
}
NetworkRegistrationHelper(config, HTTPNetworkRegistrationService(config.certificateSigningService)).buildKeystore()
}
}.map { it.join() }
doorman.close()
}
}
private fun makeTestDataSourceProperties(): Properties {
val props = Properties()
props.setProperty("dataSourceClassName", "org.h2.jdbcx.JdbcDataSource")
props.setProperty("dataSource.url", "jdbc:h2:mem:${SigningServiceIntegrationTest.DB_NAME};DB_CLOSE_DELAY=-1")
props.setProperty("dataSource.user", "sa")
props.setProperty("dataSource.password", "")
return props
}
internal fun makeNotInitialisingTestDatabaseProperties(): Properties {
val props = Properties()
props.setProperty("initDatabase", "false")
return props
}

View File

@ -0,0 +1,127 @@
package net.corda.signing
import net.corda.node.utilities.configureDatabase
import net.corda.signing.authentication.Authenticator
import net.corda.signing.authentication.createProvider
import net.corda.signing.configuration.Parameters
import net.corda.signing.configuration.parseParameters
import net.corda.signing.generator.KeyCertificateGenerator
import net.corda.signing.hsm.HsmSigner
import net.corda.signing.menu.Menu
import net.corda.signing.persistence.ApprovedCertificateRequestData
import net.corda.signing.persistence.DBCertificateRequestStorage
import net.corda.signing.persistence.SigningServerSchemaService
import net.corda.signing.utils.mapCryptoServerException
fun main(args: Array<String>) {
run(parseParameters(*args))
}
fun run(parameters: Parameters) {
parameters.run {
// Create DB connection.
checkNotNull(dataSourceProperties)
val database = configureDatabase(dataSourceProperties!!, databaseProperties, { SigningServerSchemaService() }, createIdentityService = {
// Identity service not needed
throw UnsupportedOperationException()
})
val storage = DBCertificateRequestStorage(database)
val provider = createProvider()
val sign: (List<ApprovedCertificateRequestData>) -> Unit = {
val signer = HsmSigner(
storage,
certificateName,
privateKeyPass,
rootCertificateName,
validDays,
keyStorePass,
Authenticator(provider, authMode, autoUsername, authKeyFilePath, authKeyFilePass, signAuthThreshold))
signer.sign(it)
}
Menu().withExceptionHandler(::processError).addItem("1", "Generate root and intermediate certificates", {
if (confirmedKeyGen()) {
val generator = KeyCertificateGenerator(
Authenticator(provider, authMode, autoUsername, authKeyFilePath, authKeyFilePass, keyGenAuthThreshold),
keySpecifier,
keyGroup)
generator.generateAllCertificates(keyStorePass, certificateName, privateKeyPass, rootCertificateName, rootPrivateKeyPass, validDays)
}
}).addItem("2", "Sign all approved and unsigned CSRs", {
val approved = storage.getApprovedRequests()
if (approved.isNotEmpty()) {
if (confirmedSign(approved)) {
sign(approved)
}
} else {
println("There is no approved CSR")
}
}).addItem("3", "List all approved and unsigned CSRs", {
val approved = storage.getApprovedRequests()
if (approved.isNotEmpty()) {
println("Approved CSRs:")
approved.forEachIndexed { index, item -> println("${index + 1}. ${item.request.subject}") }
Menu().withExceptionHandler(::processError).setExitOption("3", "Go back").
addItem("1", "Sign all listed CSRs", {
if (confirmedSign(approved)) {
sign(approved)
}
}, isTerminating = true).
addItem("2", "Select and sign CSRs", {
val selectedItems = getSelection(approved)
if (confirmedSign(selectedItems)) {
sign(selectedItems)
}
}, isTerminating = true).showMenu()
} else {
println("There is no approved and unsigned CSR")
}
}).showMenu()
}
}
private fun processError(exception: Exception) {
val processed = mapCryptoServerException(exception)
println("An error occured: ${processed.message}")
}
private fun confirmedSign(selectedItems: List<ApprovedCertificateRequestData>): Boolean {
println("Are you sure you want to sign the following requests:")
selectedItems.forEachIndexed { index, data ->
println("${index + 1} ${data.request.subject}")
}
var result = false
Menu().addItem("Y", "Yes", { result = true }, true).setExitOption("N", "No").showMenu()
return result
}
private fun confirmedKeyGen(): Boolean {
println("Are you sure you want to generate new keys/certificates (it will overwrite the existing ones):")
var result = false
Menu().addItem("Y", "Yes", { result = true }, true).setExitOption("N", "No").showMenu()
return result
}
private fun getSelection(toSelect: List<ApprovedCertificateRequestData>): List<ApprovedCertificateRequestData> {
print("CSRs to be signed (comma separated list): ")
val line = readLine()
if (line == null) {
println("EOF reached")
return emptyList()
}
return try {
line.split(",").map {
val result = it.toInt() - 1
if (result > toSelect.size - 1) {
throw IllegalArgumentException("Selected ${result + 1} item is out of bounds")
} else {
toSelect[result]
}
}
} catch (exception: Exception) {
println(exception.message)
emptyList()
}
}

View File

@ -0,0 +1,8 @@
package net.corda.signing.authentication
/*
* Supported authentication modes
*/
enum class AuthMode {
PASSWORD, CARD_READER, KEY_FILE
}

View File

@ -0,0 +1,144 @@
package net.corda.signing.authentication
import CryptoServerJCE.CryptoServerProvider
import net.corda.signing.configuration.Parameters
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.Console
import java.nio.file.Path
import kotlin.reflect.full.memberProperties
class Authenticator(private val provider: CryptoServerProvider,
private val mode: AuthMode = AuthMode.PASSWORD,
private val autoUsername: String? = null,
private val authKeyFilePath: Path? = null,
private val authKeyFilePass: String? = null,
private val authStrengthThreshold: Int = 2,
val console: Console? = System.console()) {
/**
* Interactively (using console) authenticates a user against the HSM. Once authentication is successful the
* [block] is executed.
* @param block to be executed once the authentication process succeeds. The block should take 2 parameters:
* 1) [CryptoServerProvider] instance
* 2) List of strings that corresponds to user names authenticated against the HSM.
*/
fun connectAndAuthenticate(block: (CryptoServerProvider, List<String>) -> Unit) {
try {
val authenticated = mutableListOf<String>()
loop@ while (true) {
val user = if (autoUsername.isNullOrEmpty()) {
print("Enter User Name (or Q to quit): ")
val input = readConsoleLine(console)
if (input != null && "q" == input.toLowerCase()) {
authenticated.clear()
break
}
input
} else {
println("Authenticating using preconfigured user name: $autoUsername")
autoUsername
}
when (mode) {
AuthMode.CARD_READER -> provider.loginSign(user, ":cs2:cyb:USB0", null)
AuthMode.KEY_FILE -> {
println("Authenticating using preconfigured key file")
val password = if (authKeyFilePass == null) {
val input = readPassword("Enter key file password (or Q to quit): ", console)
if ("q" == input.toLowerCase()) {
authenticated.clear()
break@loop
} else {
input
}
} else {
authKeyFilePass
}
provider.loginSign(user, authKeyFilePath.toString(), password)
}
AuthMode.PASSWORD -> {
val password = readPassword("Enter password (or Q to quit): ", console)
if ("q" == password.toLowerCase()) {
authenticated.clear()
break@loop
}
provider.loginPassword(user, password)
}
}
authenticated.add(user!!)
val auth = provider.cryptoServer.authState
if ((auth and 0x0000000F) >= authStrengthThreshold) {
println("Authentication sufficient")
break
} else {
println("Need more permissions. Add extra login")
}
}
if (!authenticated.isEmpty()) {
block(provider, authenticated)
}
} finally {
try {
provider.logoff()
} catch (throwable: Throwable) {
println("WARNING Exception while logging off")
throwable.printStackTrace(System.out)
}
}
}
}
/*
* Configuration class for [CryptoServerProvider]
*/
internal data class CryptoServerProviderConfig(
val Device: String = "3001@127.0.0.1",
val ConnectionTimeout: Int = 30000,
val Timeout: Int = 60000,
val EndSessionOnShutdown: Int = 1,
val KeepSessionAlive: Int = 0,
val KeyGroup: String = "*",
val KeySpecifier: Int = -1,
val StoreKeysExternal: Boolean = false
)
/**
* Creates an instance of [CryptoServerProvider] that corresponds to the HSM.
*/
fun Parameters.createProvider(): CryptoServerProvider {
val config = CryptoServerProviderConfig(
Device = device,
KeyGroup = keyGroup,
KeySpecifier = keySpecifier
)
val cfgBuffer = ByteArrayOutputStream()
val writer = cfgBuffer.writer(Charsets.UTF_8)
for (property in CryptoServerProviderConfig::class.memberProperties) {
writer.write("${property.name} = ${property.get(config)}\n")
}
writer.close()
val cfg = ByteArrayInputStream(cfgBuffer.toByteArray())
cfgBuffer.close()
val provider = CryptoServerProvider(cfg)
cfg.close()
return provider
}
/** Read password from console, do a readLine instead if console is null (e.g. when debugging in IDE). */
internal fun readPassword(fmt: String, console: Console? = System.console()): String {
return if (console != null) {
String(console.readPassword(fmt))
} else {
print(fmt)
readLine()!!
}
}
/** Read console line */
internal fun readConsoleLine(console: Console?): String? {
return if (console == null) {
readLine()
} else {
console.readLine()
}
}

View File

@ -0,0 +1,105 @@
package net.corda.signing.configuration
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigParseOptions
import joptsimple.ArgumentAcceptingOptionSpec
import joptsimple.OptionParser
import net.corda.core.internal.div
import net.corda.node.utilities.X509Utilities
import net.corda.nodeapi.config.parseAs
import net.corda.signing.authentication.AuthMode
import java.nio.file.Path
import java.nio.file.Paths
import java.util.*
class ShowHelpException(val parser: OptionParser) : Exception()
fun Array<out String>.toConfigWithOptions(registerOptions: OptionParser.() -> Unit): Config {
val parser = OptionParser()
val helpOption = parser.acceptsAll(listOf("h", "?", "help"), "show help").forHelp();
registerOptions(parser)
val optionSet = parser.parse(*this)
// Print help and exit on help option.
if (optionSet.has(helpOption)) {
throw ShowHelpException(parser)
}
// Convert all command line options to Config.
return ConfigFactory.parseMap(parser.recognizedOptions().mapValues {
val optionSpec = it.value
if (optionSpec is ArgumentAcceptingOptionSpec<*> && !optionSpec.requiresArgument() && optionSet.has(optionSpec)) true else optionSpec.value(optionSet)
}.filterValues { it != null })
}
/**
* Configuration parameters.
*/
data class Parameters(val basedir: Path = Paths.get("."),
val dataSourceProperties: Properties,
val databaseProperties: Properties? = null,
val device: String = DEFAULT_DEVICE,
val keyStorePass: String? = null,
val keyGroup: String = DEFAULT_KEY_GROUP,
val keySpecifier: Int = DEFAULT_KEY_SPECIFIER,
val rootPrivateKeyPass: String = "",
val privateKeyPass: String = "",
val certificateName: String = DEFAULT_CERTIFICATE_NAME,
val rootCertificateName: String = DEFAULT_ROOT_CERTIFICATE_NAME,
val validDays: Int = DEFAULT_VALID_DAYS,
val signAuthThreshold: Int = DEFAULT_SIGN_AUTH_THRESHOLD,
val keyGenAuthThreshold: Int = DEFAULT_KEY_GEN_AUTH_THRESHOLD,
val authMode: AuthMode = DEFAULT_AUTH_MODE,
val authKeyFilePath: Path? = DEFAULT_KEY_FILE_PATH,
val authKeyFilePass: String? = DEFAULT_KEY_FILE_PASS,
val autoUsername: String? = DEFAULT_AUTO_USERNAME) {
companion object {
val DEFAULT_DEVICE = "3001@127.0.0.1"
val DEFAULT_AUTH_MODE = AuthMode.PASSWORD
val DEFAULT_SIGN_AUTH_THRESHOLD = 2
val DEFAULT_KEY_GEN_AUTH_THRESHOLD = 2
val DEFAULT_CERTIFICATE_NAME = X509Utilities.CORDA_INTERMEDIATE_CA
val DEFAULT_ROOT_CERTIFICATE_NAME = X509Utilities.CORDA_ROOT_CA
val DEFAULT_VALID_DAYS = 3650
val DEFAULT_KEY_GROUP = "DEV.DOORMAN"
val DEFAULT_KEY_SPECIFIER = 1
val DEFAULT_KEY_FILE_PATH: Path? = null //Paths.get("/Users/michalkit/WinDev1706Eval/Shared/TEST4.key")
val DEFAULT_KEY_FILE_PASS: String? = null
val DEFAULT_AUTO_USERNAME: String? = null
}
}
/**
* Parses the list of arguments and produces an instance of [Parameters].
* @param args list of strings corresponding to program arguments
* @return instance of Parameters produced from [args]
*/
fun parseParameters(vararg args: String): Parameters {
val argConfig = args.toConfigWithOptions {
accepts("basedir", "Overriding configuration filepath, default to current directory.").withRequiredArg().defaultsTo(".").describedAs("filepath")
accepts("configFile", "Overriding configuration file. (default: <<current directory>>/node.conf)").withRequiredArg().describedAs("filepath")
accepts("device", "CryptoServer device address (default: ${Parameters.DEFAULT_DEVICE})").withRequiredArg().defaultsTo(Parameters.DEFAULT_DEVICE)
accepts("keyStorePass", "Password for the key store").withRequiredArg().describedAs("password")
accepts("keyGroup", "CryptoServer key group (default: ${Parameters.DEFAULT_KEY_GROUP})").withRequiredArg().defaultsTo(Parameters.DEFAULT_KEY_GROUP)
accepts("keySpecifier", "CryptoServer key specifier (default: ${Parameters.DEFAULT_KEY_SPECIFIER})").withRequiredArg().ofType(Int::class.java).defaultsTo(Parameters.DEFAULT_KEY_SPECIFIER)
accepts("rootPrivateKeyPass", "Password for the root certificate private key").withRequiredArg().describedAs("password")
accepts("privateKeyPass", "Password for the certificate private key").withRequiredArg().describedAs("password")
accepts("keyGenAuthThreshold", "Authentication strength threshold for the HSM key generation (default: ${Parameters.DEFAULT_KEY_GEN_AUTH_THRESHOLD})").withRequiredArg().ofType(Int::class.java).defaultsTo(Parameters.DEFAULT_KEY_GEN_AUTH_THRESHOLD)
accepts("signAuthThreshold", "Authentication strength threshold for the HSM CSR signing (default: ${Parameters.DEFAULT_SIGN_AUTH_THRESHOLD})").withRequiredArg().ofType(Int::class.java).defaultsTo(Parameters.DEFAULT_SIGN_AUTH_THRESHOLD)
accepts("authMode", "Authentication mode. Allowed values: ${AuthMode.values()} (default: ${Parameters.DEFAULT_AUTH_MODE} )").withRequiredArg().defaultsTo(Parameters.DEFAULT_AUTH_MODE.name)
accepts("authKeyFilePath", "Key file path when authentication is based on a key file (i.e. authMode=${AuthMode.KEY_FILE.name})").withRequiredArg().describedAs("filepath")
accepts("authKeyFilePass", "Key file password when authentication is based on a key file (i.e. authMode=${AuthMode.KEY_FILE.name})").withRequiredArg()
accepts("autoUsername", "Username to be used for certificate signing (if not specified it will be prompted for input)").withRequiredArg()
accepts("certificateName", "Name of the certificate to be used by this CA (default: ${Parameters.DEFAULT_CERTIFICATE_NAME})").withRequiredArg().defaultsTo(Parameters.DEFAULT_CERTIFICATE_NAME)
accepts("rootCertificateName", "Name of the root certificate to be used by this CA (default: ${Parameters.DEFAULT_ROOT_CERTIFICATE_NAME})").withRequiredArg().defaultsTo(Parameters.DEFAULT_ROOT_CERTIFICATE_NAME)
accepts("validDays", "Validity duration in days (default: ${Parameters.DEFAULT_VALID_DAYS})").withRequiredArg().ofType(Int::class.java).defaultsTo(Parameters.DEFAULT_VALID_DAYS)
}
val configFile = if (argConfig.hasPath("configFile")) {
Paths.get(argConfig.getString("configFile"))
} else {
Paths.get(argConfig.getString("basedir")) / "signing_service.conf"
}
val config = argConfig.withFallback(ConfigFactory.parseFile(configFile.toFile(), ConfigParseOptions.defaults().setAllowMissing(true))).resolve()
return config.parseAs<Parameters>()
}

View File

@ -0,0 +1,105 @@
package net.corda.signing.generator
import CryptoServerCXI.CryptoServerCXI
import CryptoServerJCE.CryptoServerProvider
import net.corda.node.utilities.addOrReplaceKey
import net.corda.signing.authentication.Authenticator
import net.corda.signing.utils.X509Utilities.createIntermediateCert
import net.corda.signing.utils.X509Utilities.createSelfSignedCACert
import net.corda.signing.utils.X509Utilities.getAndInitializeKeyStore
import net.corda.signing.utils.X509Utilities.getCleanEcdsaKeyPair
import net.corda.signing.utils.X509Utilities.retrieveCertificateAndKeys
import java.security.KeyPair
import java.security.KeyStore
import java.security.PrivateKey
/**
* Encapsulates logic for root and intermediate key/certificate generation.
*/
class KeyCertificateGenerator(private val authenticator: Authenticator,
private val keySpecifier: Int,
private val keyGroup: String) {
/**
* Generates root and intermediate key and certificates and stores them in the key store given by provider.
* If the keys and certificates already exists they will be overwritten.
* @param keyStorePassword password to the key store
* @param certificateKeyName name of the intermediate key/certificate
* @param privateKeyPassword password for the intermediate private key
* @param parentCertificateName name of the parent key/certificate
* @param parentPrivateKeyPassword password for the parent private key
* @param validDays days of certificate validity
*/
fun generateAllCertificates(keyStorePassword: String?,
certificateKeyName: String,
privateKeyPassword: String,
parentCertificateName: String,
parentPrivateKeyPassword: String,
validDays: Int) {
authenticator.connectAndAuthenticate { provider, signers ->
val keyStore = getAndInitializeKeyStore(provider, keyStorePassword)
generateRootCertificate(provider, keyStore, parentCertificateName, parentPrivateKeyPassword, validDays)
generateIntermediateCertificate(provider, keyStore, certificateKeyName, privateKeyPassword, parentCertificateName, parentPrivateKeyPassword, validDays)
}
}
/**
* Generates a root certificate
*/
private fun generateRootCertificate(provider: CryptoServerProvider,
keyStore: KeyStore,
certificateKeyName: String,
privateKeyPassword: String,
validDays: Int) {
val keyPair = generateEcdsaKeyPair(provider, keyStore, certificateKeyName, privateKeyPassword)
val selfSignedRootCertificate = createSelfSignedCACert("R3", keyPair, validDays, provider).certificate
keyStore.addOrReplaceKey(certificateKeyName, keyPair.private, privateKeyPassword.toCharArray(), arrayOf(selfSignedRootCertificate))
println("New certificate and key pair named $certificateKeyName have been generated")
}
/**
* Generates an intermediate certificate
*/
private fun generateIntermediateCertificate(provider: CryptoServerProvider,
keyStore: KeyStore,
certificateKeyName: String,
privateKeyPassword: String,
parentCertificateName: String,
parentPrivateKeyPassword: String,
validDays: Int) {
val parentCACertKey = retrieveCertificateAndKeys(parentCertificateName, parentPrivateKeyPassword, keyStore)
val keyPair = generateEcdsaKeyPair(provider, keyStore, certificateKeyName, privateKeyPassword)
val intermediateCertificate = createIntermediateCert("R3 Intermediate", parentCACertKey, keyPair, validDays, provider)
keyStore.addOrReplaceKey(certificateKeyName, keyPair.private, privateKeyPassword.toCharArray(), arrayOf(intermediateCertificate.certificate))
println("New certificate and key pair named $certificateKeyName have been generated")
}
private fun generateECDSAKey(keySpecifier: Int, keyName: String, keyGroup: String, provider: CryptoServerProvider, overwrite: Boolean = true) {
val generateFlag = if (overwrite) {
println("!!! WARNING: OVERWRITING KEY NAMED $keyName !!!")
CryptoServerCXI.FLAG_OVERWRITE
} else {
0
}
val keyAttributes = CryptoServerCXI.KeyAttributes()
keyAttributes.apply {
algo = CryptoServerCXI.KEY_ALGO_ECDSA
group = keyGroup
specifier = keySpecifier
export = 0 // deny export
name = keyName
setCurve("NIST-P256")
}
println("Generating key...")
val mechanismFlag = CryptoServerCXI.MECH_RND_REAL or CryptoServerCXI.MECH_KEYGEN_UNCOMP
provider.cryptoServer.generateKey(generateFlag, keyAttributes, mechanismFlag)
}
private fun generateEcdsaKeyPair(provider: CryptoServerProvider, keyStore: KeyStore, keyName: String, privateKeyPassword: String): KeyPair {
generateECDSAKey(keySpecifier, keyName, keyGroup, provider)
val privateKey = keyStore.getKey(keyName, privateKeyPassword.toCharArray()) as PrivateKey
val publicKey = keyStore.getCertificate(keyName).publicKey
return getCleanEcdsaKeyPair(publicKey, privateKey)
}
}

View File

@ -0,0 +1,50 @@
package net.corda.signing.hsm
import net.corda.signing.authentication.Authenticator
import net.corda.signing.authentication.readPassword
import net.corda.signing.persistence.ApprovedCertificateRequestData
import net.corda.signing.persistence.DBCertificateRequestStorage
import net.corda.signing.utils.X509Utilities.buildCertPath
import net.corda.signing.utils.X509Utilities.createClientCertificate
import net.corda.signing.utils.X509Utilities.getAndInitializeKeyStore
import net.corda.signing.utils.X509Utilities.retrieveCertificateAndKeys
/**
* Encapsulates certificate signing logic
*/
class HsmSigner(private val storage: DBCertificateRequestStorage,
private val caCertificateName: String,
private val caPrivateKeyPass: String?,
private val caParentCertificateName: String,
private val validDays: Int,
private val keyStorePassword: String?,
private val authenticator: Authenticator) : Signer {
/**
* Signs the provided list of approved certificate signing requests. By signature we mean creation of the client-level certificate
* that is accompanied with a key pair (public + private) and signed by the intermediate CA using its private key.
* That key (along with the certificate) is retrieved from the key store obtained from the provider given as a result of the
* connectAndAuthenticate method of the authenticator.
* The method iterates through the collection of the [ApprovedCertificateRequestData] instances passed as the method parameter
* and sets the certificate field with an appropriate value.
* @param toSign list of approved certificates to be signed
*/
override fun sign(toSign: List<ApprovedCertificateRequestData>) {
authenticator.connectAndAuthenticate { provider, signers ->
val keyStore = getAndInitializeKeyStore(provider, keyStorePassword)
// This should be changed once we allow for more certificates in the chain. Preferably we should use
// keyStore.getCertificateChain(String) and assume entire chain is stored in the HSM (depending on the support).
val caParentCertificate = keyStore.getCertificate(caParentCertificateName)
val caPrivateKeyPass = caPrivateKeyPass ?: readPassword("CA Private Key Password: ", authenticator.console)
val caCertAndKey = retrieveCertificateAndKeys(caCertificateName, caPrivateKeyPass, keyStore)
toSign.forEach {
it.certPath = buildCertPath(createClientCertificate(caCertAndKey, it.request, validDays, provider), caParentCertificate)
}
storage.sign(toSign, signers)
println("The following certificates have been signed by $signers:")
toSign.forEachIndexed { index, data ->
println("${index+1} ${data.request.subject}")
}
}
}
}

View File

@ -0,0 +1,15 @@
package net.corda.signing.hsm
import net.corda.signing.persistence.ApprovedCertificateRequestData
/**
* Encapsulates the logic related to the certificate signing process.
*/
interface Signer {
/**
* Signs the provided list of [ApprovedCertificateRequestData]
*/
fun sign(toSign: List<ApprovedCertificateRequestData>)
}

View File

@ -0,0 +1,99 @@
package net.corda.signing.menu
data class MenuItem(val key: String, val label: String, val action: () -> Unit, val isTerminating: Boolean = false)
/**
* A generic menu class for console based interactions with user.
* Inspired by https://github.com/bryandh/genericmenu, but adjusted to our the particular needs of this project.
*/
class Menu {
private val items = mutableMapOf<String, MenuItem>()
private var quit: Pair<String, String>? = null
private var exceptionHandler: (exception: Exception) -> Unit = { exception -> println(exception.message) }
/**
* Adds a menu item to the list of the menu items. The order in which the items are added matters.
* If the exit option is set (@see [setExitOption] then it will be displayed always as the last element in the menu.
*
* @param key itemization label. E.g. If you pass "1", it will be displayed as [1].
* @param label menu item label
* @param action lambda that is executed when user selects this option.
* @param isTerminating flag that specifies whether the completion of the action should exit this menu.
*/
fun addItem(key: String, label: String, action: () -> Unit, isTerminating: Boolean = false): Menu {
items[key.toLowerCase()] = MenuItem(key, label, action, isTerminating)
return this
}
/**
* Assigns the exception handler for this menu. The exception handler is invoked every time
* any of the menu item selection handler function throws an exception.
*
*/
fun withExceptionHandler(handler: (exception: Exception) -> Unit): Menu {
exceptionHandler = handler
return this
}
/**
* Sets the exit option with given key and label.
*/
fun setExitOption(key: String, label: String): Menu {
quit = Pair(key, label)
return this
}
/**
* Removes exit option from the list of the menu items.
*/
fun unsetExitOption(): Menu {
quit = null
return this
}
private fun printItems() {
items.forEach { _, (key, label, _) -> println("[$key] $label") }
if (quit != null) println("[${quit?.first}] ${quit?.second}")
}
private fun readInput(): String? {
print("> ")
return readLine()
}
private fun run(key: String): Boolean {
val selected = items[key.toLowerCase()]
if (selected == null) {
throw IllegalArgumentException("No valid option for $key found, try again.")
} else {
selected.action()
return selected.isTerminating
}
}
/**
* Shows the menu built out of the given menu items and (if present) exit option.
*/
fun showMenu() {
while (true) {
printItems()
val choice = readInput()
if (choice != null) {
if ((quit != null) && choice.toLowerCase() == quit!!.first.toLowerCase()) {
break
} else {
try {
if (run(choice)) {
break
}
} catch (exception: Exception) {
exceptionHandler(exception)
}
}
} else {
// No more input
break
}
}
}
}

View File

@ -0,0 +1,19 @@
package net.corda.signing.persistence
/**
* Provides an API for database level manipulations of CSRs (Certificate Signing Requests).
*/
interface CertificateRequestStorage {
/**
* Returns all certificate signing requests that have been approved for signing.
*/
fun getApprovedRequests(): List<ApprovedCertificateRequestData>
/**
* Marks the database CSR entries as signed. Also it persists the certificate and the signature in the database.
*
* @param requests Requests that are to be marked as signed.
* @param signers List of user names that signed those requests. To be specific, each request has been signed by all of those users.
*/
fun sign(requests: List<ApprovedCertificateRequestData>, signers: List<String>)
}

View File

@ -0,0 +1,99 @@
package net.corda.signing.persistence
import net.corda.node.utilities.CordaPersistence
import org.bouncycastle.pkcs.PKCS10CertificationRequest
import java.security.cert.CertPath
import java.security.cert.Certificate
import java.security.cert.CertificateFactory
import java.time.Instant
import javax.persistence.*
import javax.persistence.criteria.CriteriaBuilder
import javax.persistence.criteria.Path
import javax.persistence.criteria.Predicate
data class ApprovedCertificateRequestData(val requestId: String, val request: PKCS10CertificationRequest, var certPath: CertPath? = null)
class DBCertificateRequestStorage(private val database: CordaPersistence) : CertificateRequestStorage {
enum class Status {
Approved, Signed
}
@Entity
@Table(name = "certificate_signing_request")
class CertificateSigningRequest(
@Id
@Column(name = "request_id", length = 64)
var requestId: String = "",
@Lob
@Column
var request: ByteArray = ByteArray(0),
@Lob
@Column(nullable = true)
var certificatePath: ByteArray? = null,
@Column(name = "signed_by", length = 512)
@ElementCollection(targetClass = String::class, fetch = FetchType.EAGER)
var signedBy: List<String>? = null,
@Column(name = "signed_at")
var signedAt: Instant? = Instant.now(),
@Column(name = "status")
@Enumerated(EnumType.STRING)
var status: Status = Status.Approved
)
override fun getApprovedRequests(): List<ApprovedCertificateRequestData> {
return getRequestIdsByStatus(Status.Approved)
}
override fun sign(requests: List<ApprovedCertificateRequestData>, signers: List<String>) {
requests.forEach {
database.transaction {
val request = singleRequestWhere { builder, path ->
builder.and(
builder.equal(path.get<String>(CertificateSigningRequest::requestId.name), it.requestId),
builder.equal(path.get<String>(CertificateSigningRequest::status.name), Status.Approved)
)
}
if (request != null) {
val now = Instant.now()
request.certificatePath = it.certPath?.encoded
request.status = Status.Signed
request.signedAt = now
request.signedBy = signers
session.update(request)
}
}
}
}
private fun singleRequestWhere(predicate: (CriteriaBuilder, Path<CertificateSigningRequest>) -> Predicate): CertificateSigningRequest? {
return database.transaction {
val builder = session.criteriaBuilder
val criteriaQuery = builder.createQuery(CertificateSigningRequest::class.java)
val query = criteriaQuery.from(CertificateSigningRequest::class.java).run {
criteriaQuery.where(predicate(builder, this))
}
session.createQuery(query).uniqueResultOptional().orElse(null)
}
}
private fun getRequestIdsByStatus(status: Status): List<ApprovedCertificateRequestData> {
return database.transaction {
val builder = session.criteriaBuilder
val query = builder.createQuery(CertificateSigningRequest::class.java).run {
from(CertificateSigningRequest::class.java).run {
where(builder.equal(get<Status>(CertificateSigningRequest::status.name), status))
}
}
session.createQuery(query).resultList.map { it.toRequestData() }
}
}
private fun CertificateSigningRequest.toRequestData() = ApprovedCertificateRequestData(requestId, PKCS10CertificationRequest(request))
}

View File

@ -0,0 +1,20 @@
package net.corda.signing.persistence
import net.corda.core.contracts.ContractState
import net.corda.core.schemas.MappedSchema
import net.corda.core.schemas.PersistentState
import net.corda.node.services.api.SchemaService
class SigningServerSchemaService: SchemaService {
// Entities for compulsory services
object SigningServerServices
object SigningServerServicesV1 : MappedSchema(schemaFamily = SigningServerServices.javaClass, version = 1,
mappedTypes = listOf(DBCertificateRequestStorage.CertificateSigningRequest::class.java))
override val schemaOptions: Map<MappedSchema, SchemaService.SchemaOptions> = mapOf(Pair(SigningServerServicesV1, SchemaService.SchemaOptions()))
override fun selectSchemas(state: ContractState): Iterable<MappedSchema> = setOf(SigningServerServicesV1)
override fun generateMappedObject(state: ContractState, schema: MappedSchema): PersistentState = throw UnsupportedOperationException()
}

View File

@ -0,0 +1,40 @@
package net.corda.signing.utils
import CryptoServerAPI.CryptoServerException
import java.util.HashMap
/**
* CryptoServer error translator object.
* It holds mapping between CryptoServer error code to its human readable description.
*/
object HsmErrors {
val errors: Map<Int, String> by lazy(HsmErrors::load)
private fun load(): Map<Int, String> {
val errors = HashMap<Int, String>()
val hsmErrorsStream = HsmErrors::class.java.getResourceAsStream("hsm_errors")
hsmErrorsStream.bufferedReader().lines().reduce(null) { previous, current ->
if (previous == null) {
current
} else {
errors[java.lang.Long.decode(previous).toInt()] = current
null
}
}
return errors
}
}
/**
* Utility function for providing human readable error description in case of the [CryptoServerException] being thrown.
* If the exception is of different type the method does nothing.
*/
fun mapCryptoServerException(exception: Exception): Exception {
// Try to decode the error code
val crypto = exception as? CryptoServerException ?: exception.cause as? CryptoServerException
if (crypto != null) {
return Exception("(CryptoServer) ${HsmErrors.errors[crypto.ErrorCode]}", exception)
} else {
return exception
}
}

View File

@ -0,0 +1,303 @@
package net.corda.signing.utils
import CryptoServerJCE.CryptoServerProvider
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.toX509CertHolder
import net.corda.node.utilities.CertificateAndKeyPair
import net.corda.node.utilities.CertificateType
import net.corda.node.utilities.X509Utilities
import net.corda.node.utilities.getX509Certificate
import org.bouncycastle.asn1.ASN1EncodableVector
import org.bouncycastle.asn1.ASN1Sequence
import org.bouncycastle.asn1.DERSequence
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x500.X500NameBuilder
import org.bouncycastle.asn1.x500.style.BCStyle
import org.bouncycastle.asn1.x509.*
import org.bouncycastle.cert.X509CertificateHolder
import org.bouncycastle.cert.X509v3CertificateBuilder
import org.bouncycastle.cert.bc.BcX509ExtensionUtils
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
import org.bouncycastle.pkcs.PKCS10CertificationRequest
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
import java.math.BigInteger
import java.security.*
import java.security.cert.Certificate
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.security.spec.X509EncodedKeySpec
import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.*
object X509Utilities {
val SIGNATURE_ALGORITHM = "SHA256withECDSA"
/**
* Create a de novo root self-signed X509 v3 CA cert for the specified [KeyPair].
* @param legalName The Common (CN) field of the cert Subject will be populated with the domain string
* @param keyPair public and private keys to be associated with the generated certificate
* @param validDays number of days which this certificate is valid for
* @param provider provider to be used during the certificate signing process
* @return an instance of [CertificateAndKeyPair] class is returned containing the new root CA Cert and its [KeyPair] for signing downstream certificates.
* Note the generated certificate tree is capped at max depth of 2 to be in line with commercially available certificates
*/
fun createSelfSignedCACert(legalName: String, keyPair: KeyPair, validDays: Int, provider: Provider): CertificateAndKeyPair {
// TODO this needs to be chaneged
val issuer = getDevX509Name(legalName)
val serial = BigInteger.valueOf(random63BitValue(provider))
val subject = issuer
val pubKey = keyPair.public
// Ten year certificate validity
// TODO how do we manage certificate expiry, revocation and loss
val window = getCertificateValidityWindow(0, validDays)
val builder = JcaX509v3CertificateBuilder(
issuer, serial, window.first, window.second, subject, pubKey)
builder.addExtension(Extension.subjectKeyIdentifier, false,
createSubjectKeyIdentifier(pubKey))
// TODO to remove once we allow for longer certificate chains
builder.addExtension(Extension.basicConstraints, true,
BasicConstraints(2))
val usage = KeyUsage(KeyUsage.keyCertSign or KeyUsage.digitalSignature or KeyUsage.keyEncipherment or KeyUsage.dataEncipherment or KeyUsage.cRLSign)
builder.addExtension(Extension.keyUsage, false, usage)
val purposes = ASN1EncodableVector()
purposes.add(KeyPurposeId.id_kp_serverAuth)
purposes.add(KeyPurposeId.id_kp_clientAuth)
purposes.add(KeyPurposeId.anyExtendedKeyUsage)
builder.addExtension(Extension.extendedKeyUsage, false, DERSequence(purposes))
val cert = signCertificate(builder, keyPair.private, provider)
cert.checkValidity(Date())
cert.verify(pubKey)
return CertificateAndKeyPair(cert.toX509CertHolder(), KeyPair(pubKey, keyPair.private))
}
/**
* This is a helper function, which purpose is to workaround a bug in the bouncycastle library
* that is associated with the incorrect encoded byte production when the EC algorithm is used with the passed keys.
* @param publicKey public key
* @param privateKey private key
* @return cleaned [KeyPair] instance
*/
fun getCleanEcdsaKeyPair(publicKey: PublicKey, privateKey: PrivateKey): KeyPair {
val rawPublicKeyBytes = publicKey.encoded
val kf = KeyFactory.getInstance("EC")
val cleanPublicKey = kf.generatePublic(X509EncodedKeySpec(rawPublicKeyBytes))
return KeyPair(cleanPublicKey, privateKey)
}
/**
* Retrieves a certificate and keys from the given key store. Also, the keys retrieved are cleaned in a sense of the
* [getCleanEcdsaKeyPair] method.
* @param certificateKeyName certificate and key name (alias) to be used when querying the key store.
* @param privateKeyPassword password for the private key.
* @param keyStore key store that holds the certificate with its keys.
* @return instance of [CertificateAndKeyPair] holding the retrieved certificate with its keys.
*/
fun retrieveCertificateAndKeys(certificateKeyName: String, privateKeyPassword: String, keyStore: KeyStore): CertificateAndKeyPair {
val privateKey = keyStore.getKey(certificateKeyName, privateKeyPassword.toCharArray()) as PrivateKey
val publicKey = keyStore.getCertificate(certificateKeyName).publicKey
val certificate = keyStore.getX509Certificate(certificateKeyName)
return CertificateAndKeyPair(certificate, getCleanEcdsaKeyPair(publicKey, privateKey))
}
/**
* Create a de novo root intermediate X509 v3 CA cert and KeyPair.
* @param commonName The Common (CN) field of the cert Subject will be populated with the domain string.
* @param certificateAuthority The Public certificate and KeyPair of the root CA certificate above this used to sign it.
* @param keyPair public and private keys to be associated with the generated certificate
* @param validDays number of days which this certificate is valid for
* @param provider provider to be used during the certificate signing process
* @return an instance of [CertificateAndKeyPair] class is returned containing the new intermediate CA Cert and its KeyPair for signing downstream certificates.
* Note the generated certificate tree is capped at max depth of 1 below this to be in line with commercially available certificates
*/
fun createIntermediateCert(commonName: String,
certificateAuthority: CertificateAndKeyPair,
keyPair: KeyPair, validDays: Int, provider: Provider): CertificateAndKeyPair {
val issuer = X509CertificateHolder(certificateAuthority.certificate.encoded).subject
val serial = BigInteger.valueOf(random63BitValue(provider))
val subject = getDevX509Name(commonName)
val pubKey = keyPair.public
// Ten year certificate validity
// TODO how do we manage certificate expiry, revocation and loss
val window = getCertificateValidityWindow(0, validDays, certificateAuthority.certificate.notBefore, certificateAuthority.certificate.notAfter)
val builder = JcaX509v3CertificateBuilder(
issuer, serial, window.first, window.second, subject, pubKey)
builder.addExtension(Extension.subjectKeyIdentifier, false,
createSubjectKeyIdentifier(pubKey))
// TODO to remove onece we allow for longer certificate chains
builder.addExtension(Extension.basicConstraints, true,
BasicConstraints(1))
val usage = KeyUsage(KeyUsage.keyCertSign or KeyUsage.digitalSignature or KeyUsage.keyEncipherment or KeyUsage.dataEncipherment or KeyUsage.cRLSign)
builder.addExtension(Extension.keyUsage, false, usage)
val purposes = ASN1EncodableVector()
purposes.add(KeyPurposeId.id_kp_serverAuth)
purposes.add(KeyPurposeId.id_kp_clientAuth)
purposes.add(KeyPurposeId.anyExtendedKeyUsage)
builder.addExtension(Extension.extendedKeyUsage, false,
DERSequence(purposes))
val cert = signCertificate(builder, certificateAuthority.keyPair.private, provider)
cert.checkValidity(Date())
cert.verify(certificateAuthority.keyPair.public)
return CertificateAndKeyPair(cert.toX509CertHolder(), KeyPair(pubKey, keyPair.private))
}
/**
* Creates and signs a X509 v3 client certificate.
* @param caCertAndKey signing certificate authority certificate and its keys
* @param request certficate signing request
* @param validDays number of days which this certificate is valid for
* @param provider provider to be used during the certificate signing process
* @return an instance of [CertificateAndKeyPair] class is returned containing the signed client certificate.
*/
fun createClientCertificate(caCertAndKey: CertificateAndKeyPair,
request: PKCS10CertificationRequest,
validDays: Int,
provider: Provider): Certificate {
val jcaRequest = JcaPKCS10CertificationRequest(request)
// This can be adjusted more to our future needs.
val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, CordaX500Name.build(jcaRequest.subject).copy(commonName = null).x500Name))), arrayOf())
val issuerCertificate = caCertAndKey.certificate
val issuerKeyPair = caCertAndKey.keyPair
val certificateType = CertificateType.CLIENT_CA
val validityWindow = getCertificateValidityWindow(0, validDays, issuerCertificate.notBefore, issuerCertificate.notAfter)
val serial = BigInteger.valueOf(random63BitValue(provider))
val subject = CordaX500Name.build(jcaRequest.subject).copy(commonName = X509Utilities.CORDA_CLIENT_CA_CN).x500Name
val subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(ASN1Sequence.getInstance(jcaRequest.publicKey.encoded))
val keyPurposes = DERSequence(ASN1EncodableVector().apply { certificateType.purposes.forEach { add(it) } })
val builder = JcaX509v3CertificateBuilder(issuerCertificate.subject, serial, validityWindow.first, validityWindow.second, subject, jcaRequest.publicKey)
.addExtension(Extension.subjectKeyIdentifier, false, BcX509ExtensionUtils().createSubjectKeyIdentifier(subjectPublicKeyInfo))
.addExtension(Extension.basicConstraints, certificateType.isCA, BasicConstraints(certificateType.isCA))
.addExtension(Extension.keyUsage, false, certificateType.keyUsage)
.addExtension(Extension.extendedKeyUsage, false, keyPurposes)
.addExtension(Extension.nameConstraints, true, nameConstraints)
val certificate = signCertificate(builder, issuerKeyPair.private, provider)
certificate.checkValidity(Date())
certificate.verify(issuerKeyPair.public)
return certificate
}
/**
* Helper method to get a notBefore and notAfter pair from current day bounded by parent certificate validity range
* @param daysBefore number of days to roll back returned start date relative to current date
* @param daysAfter number of days to roll forward returned end date relative to current date
* @param parentNotBefore if provided is used to lower bound the date interval returned
* @param parentNotAfter if provided is used to upper bound the date interval returned
* Note we use Date rather than LocalDate as the consuming java.security and BouncyCastle certificate apis all use Date
* Thus we avoid too many round trip conversions.
*/
fun getCertificateValidityWindow(daysBefore: Int, daysAfter: Int, parentNotBefore: Date? = null, parentNotAfter: Date? = null): Pair<Date, Date> {
val startOfDayUTC = Instant.now().truncatedTo(ChronoUnit.DAYS)
var notBefore = Date.from(startOfDayUTC.minus(daysBefore.toLong(), ChronoUnit.DAYS))
if (parentNotBefore != null) {
if (parentNotBefore.after(notBefore)) {
notBefore = parentNotBefore
}
}
var notAfter = Date.from(startOfDayUTC.plus(daysAfter.toLong(), ChronoUnit.DAYS))
if (parentNotAfter != null) {
if (parentNotAfter.after(notAfter)) {
notAfter = parentNotAfter
}
}
return Pair(notBefore, notAfter)
}
/**
* A utility method for transforming number of certificates into the [CertPath] instance.
* The certificates passed should be ordered starting with the leaf certificate and ending with the root one.
* @param certificates ordered certficates
*/
fun buildCertPath(vararg certificates: Certificate) = CertificateFactory.getInstance("X509").generateCertPath(certificates.asList())
/**
* Creates and initializes a key store from the given crypto server provider.
* It uses the provided key store password to enable key store access.
* @param provider crypto server provider to be used for the key store creation
* @param keyStorePassword key store password to be used for key store access authentication
* @return created key store instance
*/
fun getAndInitializeKeyStore(provider: CryptoServerProvider, keyStorePassword: String?): KeyStore {
val keyStore = KeyStore.getInstance("CryptoServer", provider)
keyStore.load(null, keyStorePassword?.toCharArray())
return keyStore
}
/**
* Encode provided public key in correct format for inclusion in certificate issuer/subject fields
*/
private fun createSubjectKeyIdentifier(key: Key): SubjectKeyIdentifier {
val info = SubjectPublicKeyInfo.getInstance(key.encoded)
return BcX509ExtensionUtils().createSubjectKeyIdentifier(info)
}
/**
* Generate a random value using the provider.
*/
private fun random63BitValue(provider: Provider): Long = Math.abs(newSecureRandom(provider).nextLong())
private fun newSecureRandom(provider: Provider? = null): SecureRandom {
if (provider != null && provider.name == "CryptoServer") {
return SecureRandom.getInstance("CryptoServer", provider)
}
if (System.getProperty("os.name") == "Linux") {
return SecureRandom.getInstance("NativePRNGNonBlocking")
} else {
return SecureRandom.getInstanceStrong()
}
}
/**
* Use bouncy castle utilities to sign completed X509 certificate with CA cert private key
*/
private fun signCertificate(certificateBuilder: X509v3CertificateBuilder,
signedWithPrivateKey: PrivateKey,
provider: Provider,
signatureAlgorithm: String = SIGNATURE_ALGORITHM): X509Certificate {
val signer = JcaContentSignerBuilder(signatureAlgorithm).setProvider(provider).build(signedWithPrivateKey)
return JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certificateBuilder.build(signer))
}
/**
* Return a bogus X509 for dev purposes. Use [getX509Name] for something more real.
*/
private fun getDevX509Name(commonName: String): X500Name {
val nameBuilder = X500NameBuilder(BCStyle.INSTANCE)
nameBuilder.addRDN(BCStyle.CN, commonName)
nameBuilder.addRDN(BCStyle.O, "R3")
nameBuilder.addRDN(BCStyle.OU, "corda")
nameBuilder.addRDN(BCStyle.L, "London")
nameBuilder.addRDN(BCStyle.C, "UK")
return nameBuilder.build()
}
private fun getX509Name(myLegalName: String, nearestCity: String, email: String): X500Name {
return X500NameBuilder(BCStyle.INSTANCE)
.addRDN(BCStyle.CN, myLegalName)
.addRDN(BCStyle.L, nearestCity)
.addRDN(BCStyle.E, email).build()
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,12 @@
device = "3001@127.0.0.1"
keyGroup = "*"
keySpecifier = -1
authMode = PASSWORD
h2port = 0
dataSourceProperties {
"dataSourceClassName" = org.h2.jdbcx.JdbcDataSource
"dataSource.url" = "jdbc:h2:file:"${basedir}"/persistence;DB_CLOSE_ON_EXIT=FALSE;LOCK_TIMEOUT=10000;WRITE_DELAY=0;AUTO_SERVER_PORT="${h2port}
"dataSource.user" = sa
"dataSource.password" = ""
}

View File

@ -0,0 +1,109 @@
package net.corda.signing.authentication
import CryptoServerCXI.CryptoServerCXI
import CryptoServerJCE.CryptoServerProvider
import com.nhaarman.mockito_kotlin.*
import org.junit.Before
import org.junit.Test
import java.io.Console
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class AuthenticatorTest {
private lateinit var provider: CryptoServerProvider
private lateinit var console: Console
@Before
fun setUp() {
provider = mock()
whenever(provider.cryptoServer).thenReturn(mock<CryptoServerCXI>())
console = mock()
}
@Test
fun `connectAndAuthenticate aborts when user inputs Q`() {
// given
givenUserConsoleInputOnReadLine("Q")
var executed = false
// when
Authenticator(provider = provider, console = console).connectAndAuthenticate { _, _ -> executed = true }
// then
assertFalse(executed)
verify(provider, never()).loginPassword(any<String>(), any<String>())
verify(provider, never()).loginSign(any<String>(), any<String>(), any<String>())
}
@Test
fun `connectAndAuthenticate authenticates user with password`() {
// given
val username = "TEST_USER"
val password = "TEST_PASSWORD"
givenUserConsoleInputOnReadLine(username)
givenUserConsoleInputOnReadPassword(password)
givenAuthenticationResult(true)
var executed = false
// when
Authenticator(provider = provider, console = console).connectAndAuthenticate { _, _ -> executed = true }
// then
verify(provider).loginPassword(username, password)
verify(provider, never()).loginSign(any<String>(), any<String>(), any<String>())
assertTrue(executed)
}
@Test
fun `connectAndAuthenticate authenticates user with card reader`() {
// given
val username = "TEST_USER"
givenUserConsoleInputOnReadLine(username)
givenAuthenticationResult(true)
var executed = false
// when
Authenticator(provider = provider, console = console, mode = AuthMode.CARD_READER).connectAndAuthenticate { _, _ -> executed = true }
// then
verify(provider).loginSign(username, ":cs2:cyb:USB0", null)
verify(provider, never()).loginPassword(any<String>(), any<String>())
assertTrue(executed)
}
@Test
fun `connectAndAuthenticate authenticates multiple users with password`() {
// given
val username = "TEST_USER"
val password = "TEST_PASSWORD"
givenUserConsoleInputOnReadLine(username)
givenUserConsoleInputOnReadPassword(password)
givenAuthenticationResult(false, false, true)
var executed = false
// when
Authenticator(provider = provider, console = console).connectAndAuthenticate { _, _ -> executed = true }
// then
verify(provider, times(3)).loginPassword(username, password)
verify(provider, never()).loginSign(any<String>(), any<String>(), any<String>())
assertTrue(executed)
}
private fun givenAuthenticationResult(sufficient: Boolean, vararg subsequent: Boolean) {
val stub = whenever(provider.cryptoServer.authState).thenReturn(if (sufficient) 3 else 0)
subsequent.forEach {
stub.thenReturn(if (it) 3 else 0)
}
}
private fun givenUserConsoleInputOnReadPassword(input: String) {
whenever(console.readPassword(any<String>())).thenReturn(input.toCharArray())
}
private fun givenUserConsoleInputOnReadLine(input: String) {
whenever(console.readLine()).thenReturn(input)
}
}

View File

@ -0,0 +1,35 @@
package net.corda.signing.configuration
import com.typesafe.config.ConfigException
import net.corda.signing.authentication.AuthMode
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
class ConfigurationTest {
private val validConfigPath = javaClass.getResource("/signing_service.conf").path
private val invalidConfigPath = javaClass.getResource("/signing_service_fail.conf").path
@Test
fun `authMode is parsed correctly`() {
val paramsWithPassword = parseParameters("--configFile", validConfigPath, "--authMode", AuthMode.CARD_READER.name)
assertEquals(AuthMode.CARD_READER, paramsWithPassword.authMode)
val paramsWithCardReader = parseParameters("--configFile", validConfigPath, "--authMode", AuthMode.PASSWORD.name)
assertEquals(AuthMode.PASSWORD, paramsWithCardReader.authMode)
}
@Test
fun `validDays duration is parsed correctly`() {
val expectedDuration = 360
val paramsWithPassword = parseParameters("--configFile", validConfigPath, "--validDays", expectedDuration.toString())
assertEquals(expectedDuration, paramsWithPassword.validDays)
}
@Test
fun `should fail when config missing database source properties`() {
// dataSourceProperties is missing from node_fail.conf and it should fail during parsing, and shouldn't use default from reference.conf.
assertFailsWith<ConfigException.Missing> {
parseParameters("--configFile", invalidConfigPath)
}
}
}

View File

@ -0,0 +1,172 @@
package net.corda.signing.persistence
import com.r3.corda.doorman.buildCertPath
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.CordaX500Name
import net.corda.node.utilities.CertificateType
import net.corda.node.utilities.CordaPersistence
import net.corda.node.utilities.X509Utilities
import net.corda.node.utilities.configureDatabase
import net.corda.signing.persistence.DBCertificateRequestStorage.CertificateSigningRequest
import net.corda.signing.persistence.DBCertificateRequestStorage.Status
import org.bouncycastle.asn1.x509.GeneralName
import org.bouncycastle.asn1.x509.GeneralSubtree
import org.bouncycastle.asn1.x509.NameConstraints
import org.bouncycastle.cert.X509CertificateHolder
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
import org.junit.After
import org.junit.Before
import org.junit.Test
import java.io.ByteArrayInputStream
import java.security.cert.Certificate
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.util.*
import javax.persistence.criteria.CriteriaBuilder
import javax.persistence.criteria.Path
import javax.persistence.criteria.Predicate
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
class DBCertificateRequestStorageTest {
private val intermediateCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
private val intermediateCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Corda Node Intermediate CA", organisation = "R3 Ltd", locality = "London", country = "GB").x500Name, intermediateCAKey)
private lateinit var storage: DBCertificateRequestStorage
private lateinit var persistence: CordaPersistence
@Before
fun startDb() {
persistence = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), { SigningServerSchemaService() }, createIdentityService = { throw UnsupportedOperationException() })
storage = DBCertificateRequestStorage(persistence)
}
@After
fun closeDb() {
persistence.close()
}
@Test
fun `getApprovedRequests returns only requests with status APPROVED`() {
// given
(1..10).forEach {
createAndPersistRequest("Bank$it", Status.Approved)
}
(11..15).forEach {
createAndPersistRequest("Bank$it", Status.Signed)
}
// when
val result = storage.getApprovedRequests()
// then
assertEquals(10, result.size)
result.forEach {
val request = getRequestById(it.requestId)
assertNotNull(request)
assertEquals(Status.Approved, request?.status)
}
}
@Test
fun `sign changes the status of requests to SIGNED`() {
// given
(1..10).map {
createAndPersistRequest("Bank$it")
}
val requests = storage.getApprovedRequests()
// Create a signed certificate
requests.forEach { certifyAndSign(it) }
val signers = listOf("TestUserA", "TestUserB")
// when
storage.sign(requests, signers)
// then
requests.forEach {
val request = getRequestById(it.requestId)
assertNotNull(request)
assertEquals(Status.Signed, request?.status)
assertEquals(signers.toString(), request?.signedBy.toString())
assertNotNull(request?.certificatePath)
}
}
private fun certifyAndSign(approvedRequestData: ApprovedCertificateRequestData) {
JcaPKCS10CertificationRequest(approvedRequestData.request).run {
val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, subject))), arrayOf())
approvedRequestData.certPath = buildCertPath(
X509Utilities.createCertificate(
CertificateType.CLIENT_CA,
intermediateCACert,
intermediateCAKey,
subject,
publicKey,
nameConstraints = nameConstraints).toX509Certificate())
}
}
private fun getRequestById(requestId: String): CertificateSigningRequest? {
return persistence.transaction {
singleRequestWhere { builder, path ->
builder.equal(path.get<String>(CertificateSigningRequest::requestId.name), requestId)
}
}
}
private fun singleRequestWhere(predicate: (CriteriaBuilder, Path<CertificateSigningRequest>) -> Predicate): CertificateSigningRequest? {
return persistence.transaction {
val builder = session.criteriaBuilder
val criteriaQuery = builder.createQuery(CertificateSigningRequest::class.java)
val query = criteriaQuery.from(CertificateSigningRequest::class.java).run {
criteriaQuery.where(predicate(builder, this))
}
session.createQuery(query).uniqueResultOptional().orElse(null)
}
}
private fun createAndPersistRequest(legalName: String, status: Status = Status.Approved): String {
val requestId = SecureHash.randomSHA256().toString()
persistence.transaction {
val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val x500Name = CordaX500Name(organisation = legalName, locality = "London", country = "GB").x500Name
session.save(CertificateSigningRequest(
requestId = requestId,
status = status,
request = X509Utilities.createCertificateSigningRequest(x500Name, "my@mail.com", keyPair).encoded
))
}
return requestId
}
private fun makeTestDataSourceProperties(nodeName: String = SecureHash.randomSHA256().toString()): Properties {
val props = Properties()
props.setProperty("dataSourceClassName", "org.h2.jdbcx.JdbcDataSource")
props.setProperty("dataSource.url", "jdbc:h2:mem:${nodeName}_persistence;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE")
props.setProperty("dataSource.user", "sa")
props.setProperty("dataSource.password", "")
return props
}
private fun makeTestDatabaseProperties(key: String? = null, value: String? = null): Properties {
val props = Properties()
props.setProperty("transactionIsolationLevel", "repeatableRead") //for other possible values see net.corda.node.utilities.CordaPeristence.parserTransactionIsolationLevel(String)
if (key != null) {
props.setProperty(key, value)
}
return props
}
}
private object CertificateUtilities {
fun toX509Certificate(byteArray: ByteArray): X509Certificate {
return CertificateFactory.getInstance("X509").generateCertificate(ByteArrayInputStream(byteArray)) as X509Certificate
}
}
/**
* Converts [X509CertificateHolder] to standard Java [Certificate]
*/
private fun X509CertificateHolder.toX509Certificate(): Certificate = CertificateUtilities.toX509Certificate(encoded)

View File

@ -0,0 +1 @@
mock-maker-inline

View File

@ -0,0 +1,12 @@
device = "3001@127.0.0.1"
keyGroup = "*"
keySpecifier = -1
authMode = PASSWORD
h2port = 0
dataSourceProperties {
"dataSourceClassName" = org.h2.jdbcx.JdbcDataSource
"dataSource.url" = "jdbc:h2:file:"${basedir}"/persistence;DB_CLOSE_ON_EXIT=FALSE;LOCK_TIMEOUT=10000;WRITE_DELAY=0;AUTO_SERVER_PORT="${h2port}
"dataSource.user" = sa
"dataSource.password" = ""
}

View File

@ -0,0 +1,4 @@
device = "3001@127.0.0.1"
keyGroup = "*"
keySpecifier = -1
authMode = PASSWORD