mirror of
https://github.com/corda/corda.git
synced 2025-01-15 09:20:22 +00:00
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:
parent
e393fdd292
commit
e22570a81d
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -45,3 +45,5 @@ include 'cordform-common'
|
||||
include 'doorman'
|
||||
include 'verify-enclave'
|
||||
include 'sgx-jvm/hsm-tool'
|
||||
include 'signing-server'
|
||||
|
||||
|
88
signing-server/build.gradle
Normal file
88
signing-server/build.gradle
Normal 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')
|
||||
}
|
BIN
signing-server/libs/CryptoServerCXI.jar
Normal file
BIN
signing-server/libs/CryptoServerCXI.jar
Normal file
Binary file not shown.
BIN
signing-server/libs/CryptoServerJCE.jar
Normal file
BIN
signing-server/libs/CryptoServerJCE.jar
Normal file
Binary file not shown.
@ -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
|
||||
}
|
@ -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
|
||||
}
|
127
signing-server/src/main/kotlin/net/corda/signing/Main.kt
Normal file
127
signing-server/src/main/kotlin/net/corda/signing/Main.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,8 @@
|
||||
package net.corda.signing.authentication
|
||||
|
||||
/*
|
||||
* Supported authentication modes
|
||||
*/
|
||||
enum class AuthMode {
|
||||
PASSWORD, CARD_READER, KEY_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()
|
||||
}
|
||||
}
|
@ -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>()
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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>)
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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>)
|
||||
}
|
@ -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))
|
||||
}
|
@ -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()
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
4462
signing-server/src/main/resources/net/corda/signing/utils/hsm_errors
Normal file
4462
signing-server/src/main/resources/net/corda/signing/utils/hsm_errors
Normal file
File diff suppressed because it is too large
Load Diff
12
signing-server/src/main/resources/reference.conf
Normal file
12
signing-server/src/main/resources/reference.conf
Normal 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" = ""
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
@ -0,0 +1 @@
|
||||
mock-maker-inline
|
12
signing-server/src/test/resources/signing_service.conf
Normal file
12
signing-server/src/test/resources/signing_service.conf
Normal 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" = ""
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
device = "3001@127.0.0.1"
|
||||
keyGroup = "*"
|
||||
keySpecifier = -1
|
||||
authMode = PASSWORD
|
Loading…
Reference in New Issue
Block a user