Uses config file instead of passing in arguments

Added H2 DB persistence
Added background thread to approve request periodically
This commit is contained in:
Patrick Kuo 2016-11-01 10:23:26 +00:00
parent b9b90dffcd
commit 549a952cc0
9 changed files with 302 additions and 54 deletions

View File

@ -23,10 +23,25 @@ task buildCertSignerJAR(type: FatCapsule, dependsOn: 'jar') {
} }
} }
sourceSets {
main {
resources {
srcDir "../config/dev"
}
}
test {
resources {
srcDir "../config/test"
}
}
}
dependencies { dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
compile project(":core") compile project(":core")
compile project(":node")
testCompile project(":test-utils")
// Log4J: logging framework (with SLF4J bindings) // Log4J: logging framework (with SLF4J bindings)
compile "org.apache.logging.log4j:log4j-slf4j-impl:${log4j_version}" compile "org.apache.logging.log4j:log4j-slf4j-impl:${log4j_version}"
@ -46,6 +61,9 @@ dependencies {
// JOpt: for command line flags. // JOpt: for command line flags.
compile "net.sf.jopt-simple:jopt-simple:5.0.2" 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. // Unit testing helpers.
testCompile 'junit:junit:4.12' testCompile 'junit:junit:4.12'
testCompile "org.assertj:assertj-core:${assertj_version}" testCompile "org.assertj:assertj-core:${assertj_version}"

View File

@ -2,11 +2,16 @@ package com.r3corda.netpermission
import com.google.common.net.HostAndPort import com.google.common.net.HostAndPort
import com.r3corda.core.crypto.X509Utilities import com.r3corda.core.crypto.X509Utilities
import com.r3corda.core.utilities.LogHelper import com.r3corda.core.utilities.debug
import com.r3corda.core.utilities.loggerFor import com.r3corda.core.utilities.loggerFor
import com.r3corda.netpermission.internal.CertificateSigningService import com.r3corda.netpermission.internal.CertificateSigningService
import com.r3corda.netpermission.internal.persistence.InMemoryCertificationRequestStorage import com.r3corda.netpermission.internal.persistence.DBCertificateRequestStorage
import com.r3corda.node.services.config.ConfigHelper
import com.r3corda.node.services.config.getProperties
import com.r3corda.node.utilities.configureDatabase
import joptsimple.ArgumentAcceptingOptionSpec
import joptsimple.OptionParser import joptsimple.OptionParser
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
import org.eclipse.jetty.server.Server import org.eclipse.jetty.server.Server
import org.eclipse.jetty.server.ServerConnector import org.eclipse.jetty.server.ServerConnector
import org.eclipse.jetty.server.handler.HandlerCollection import org.eclipse.jetty.server.handler.HandlerCollection
@ -17,6 +22,7 @@ import org.glassfish.jersey.servlet.ServletContainer
import java.io.Closeable import java.io.Closeable
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.nio.file.Paths import java.nio.file.Paths
import kotlin.concurrent.thread
import kotlin.system.exitProcess import kotlin.system.exitProcess
/** /**
@ -25,6 +31,7 @@ import kotlin.system.exitProcess
* The Intermediate CA certificate,Intermediate CA private key and Root CA Certificate should use alias name specified in [X509Utilities] * The Intermediate CA certificate,Intermediate CA private key and Root CA Certificate should use alias name specified in [X509Utilities]
*/ */
class CertificateSigningServer(val webServerAddr: HostAndPort, val certSigningService: CertificateSigningService) : Closeable { class CertificateSigningServer(val webServerAddr: HostAndPort, val certSigningService: CertificateSigningService) : Closeable {
companion object { companion object {
val log = loggerFor<CertificateSigningServer>() val log = loggerFor<CertificateSigningServer>()
fun Server.hostAndPort(): HostAndPort { fun Server.hostAndPort(): HostAndPort {
@ -68,20 +75,11 @@ class CertificateSigningServer(val webServerAddr: HostAndPort, val certSigningSe
object ParamsSpec { object ParamsSpec {
val parser = OptionParser() val parser = OptionParser()
val host = parser.accepts("host", "The hostname permissioning server will be running on.") val basedir: ArgumentAcceptingOptionSpec<String>? = parser.accepts("basedir", "Overriding configuration file path.")
.withRequiredArg().defaultsTo("localhost") .withRequiredArg()
val port = parser.accepts("port", "The port number permissioning server will be running on.")
.withRequiredArg().ofType(Int::class.java).defaultsTo(0)
val keystorePath = parser.accepts("keystore", "The path to the keyStore containing and root certificate, intermediate CA certificate and private key.")
.withRequiredArg().required()
val storePassword = parser.accepts("storePassword", "Keystore's password.")
.withRequiredArg().required()
val caKeyPassword = parser.accepts("caKeyPassword", "Intermediate CA private key password.")
.withRequiredArg().required()
} }
fun main(args: Array<String>) { fun main(args: Array<String>) {
LogHelper.setLevel(CertificateSigningServer::class)
val log = CertificateSigningServer.log val log = CertificateSigningServer.log
log.info("Starting certificate signing server.") log.info("Starting certificate signing server.")
try { try {
@ -91,17 +89,48 @@ fun main(args: Array<String>) {
ParamsSpec.parser.printHelpOn(System.out) ParamsSpec.parser.printHelpOn(System.out)
exitProcess(1) exitProcess(1)
}.run { }.run {
// Load keystore from input path, default to Dev keystore from jar resource if path not defined. val basedir = Paths.get(valueOf(ParamsSpec.basedir) ?: ".")
val storePassword = valueOf(ParamsSpec.storePassword) val config = ConfigHelper.loadConfig(basedir)
val keyPassword = valueOf(ParamsSpec.caKeyPassword)
val keystore = X509Utilities.loadKeyStore(Paths.get(valueOf(ParamsSpec.keystorePath)).normalize(), storePassword) val keystore = X509Utilities.loadKeyStore(Paths.get(config.getString("keystorePath")).normalize(), config.getString("keyStorePassword"))
val intermediateCACertAndKey = X509Utilities.loadCertificateAndKey(keystore, keyPassword, X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY) val intermediateCACertAndKey = X509Utilities.loadCertificateAndKey(keystore, config.getString("caKeyPassword"), X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY)
val rootCA = keystore.getCertificateChain(X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY).last() val rootCA = keystore.getCertificateChain(X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY).last()
// TODO: Create a proper request storage using database or other storage technology. // Create DB connection.
val service = CertificateSigningService(intermediateCACertAndKey, rootCA, InMemoryCertificationRequestStorage()) val (datasource, database) = configureDatabase(config.getProperties("dataSourceProperties"))
CertificateSigningServer(HostAndPort.fromParts(valueOf(ParamsSpec.host), valueOf(ParamsSpec.port)), service).use { val storage = DBCertificateRequestStorage(database)
val service = CertificateSigningService(intermediateCACertAndKey, rootCA, storage)
// Background thread approving all request periodically.
var stopSigner = false
val certSinger = if (config.getBoolean("approveAll")) {
thread {
while (!stopSigner) {
Thread.sleep(1000)
for (id in storage.pendingRequestIds()) {
storage.saveCertificate(id, {
JcaPKCS10CertificationRequest(it.request).run {
X509Utilities.createServerCert(subject, publicKey, intermediateCACertAndKey,
if (it.ipAddr == it.hostName) listOf() else listOf(it.hostName), listOf(it.ipAddr))
}
})
log.debug { "Approved $id" }
}
}
log.debug { "Certificate Signer thread stopped." }
}
} else {
null
}
CertificateSigningServer(HostAndPort.fromParts(config.getString("host"), config.getInt("port")), service).use {
Runtime.getRuntime().addShutdownHook(thread(false) {
stopSigner = true
certSinger?.join()
it.close()
datasource.close()
})
it.server.join() it.server.join()
} }
} }

View File

@ -53,14 +53,8 @@ class CertificateSigningService(val intermediateCACertAndKey: X509Utilities.CACe
@Path("certificate/{var}") @Path("certificate/{var}")
@Produces(MediaType.APPLICATION_OCTET_STREAM) @Produces(MediaType.APPLICATION_OCTET_STREAM)
fun retrieveCert(@PathParam("var") requestId: String): Response { fun retrieveCert(@PathParam("var") requestId: String): Response {
val data = storage.getApprovedRequest(requestId) val clientCert = storage.getCertificate(requestId)
return if (data != null) { return if (clientCert != null) {
val clientCert = storage.getOrElseCreateCertificate(requestId) {
JcaPKCS10CertificationRequest(data.request).run {
X509Utilities.createServerCert(subject, publicKey, intermediateCACertAndKey,
if (data.ipAddr == data.hostName) listOf() else listOf(data.hostName), listOf(data.ipAddr))
}
}
// Write certificate chain to a zip stream and extract the bit array output. // Write certificate chain to a zip stream and extract the bit array output.
ByteArrayOutputStream().use { ByteArrayOutputStream().use {
ZipOutputStream(it).use { ZipOutputStream(it).use {

View File

@ -2,6 +2,7 @@ package com.r3corda.netpermission.internal.persistence
import org.bouncycastle.pkcs.PKCS10CertificationRequest import org.bouncycastle.pkcs.PKCS10CertificationRequest
import java.security.cert.Certificate import java.security.cert.Certificate
/** /**
* Provide certificate signing request storage for the certificate signing server. * Provide certificate signing request storage for the certificate signing server.
*/ */
@ -12,16 +13,25 @@ interface CertificationRequestStorage {
fun saveRequest(certificationData: CertificationData): String fun saveRequest(certificationData: CertificationData): String
/** /**
* Retrieve approved certificate singing request and Host/IP information using [requestId]. * Retrieve certificate singing request and Host/IP information using [requestId].
* Returns [CertificationData] if request has been approved, else returns null.
*/ */
fun getApprovedRequest(requestId: String): CertificationData? fun getRequest(requestId: String): CertificationData?
/** /**
* Retrieve client certificate with provided [requestId]. * Retrieve client certificate with provided [requestId].
* Generate new certificate and store in storage using provided [certificateGenerator] if certificate does not exist.
*/ */
fun getOrElseCreateCertificate(requestId: String, certificateGenerator: () -> Certificate): Certificate fun getCertificate(requestId: String): Certificate?
/**
* Generate new certificate and store in storage using provided [certificateGenerator].
*/
fun saveCertificate(requestId: String, certificateGenerator: (CertificationData) -> Certificate)
/**
* Retrieve list of request IDs waiting for approval.
* TODO : This is use for the background thread to approve request automatically without KYC checks, should be removed after testnet.
*/
fun pendingRequestIds(): List<String>
} }
data class CertificationData(val hostName: String, val ipAddr: String, val request: PKCS10CertificationRequest) data class CertificationData(val hostName: String, val ipAddr: String, val request: PKCS10CertificationRequest)

View File

@ -0,0 +1,79 @@
package com.r3corda.netpermission.internal.persistence
import com.r3corda.core.crypto.SecureHash
import com.r3corda.node.utilities.databaseTransaction
import com.r3corda.node.utilities.deserializeFromBlob
import com.r3corda.node.utilities.localDateTime
import com.r3corda.node.utilities.serializeToBlob
import org.jetbrains.exposed.sql.*
import java.security.cert.Certificate
import java.time.LocalDateTime
class DBCertificateRequestStorage(private val database: Database) : CertificationRequestStorage {
private object DataTable : Table("certificate_signing_request") {
val requestId = varchar("request_id", 64).index().primaryKey()
val hostName = varchar("hostName", 100)
val ipAddress = varchar("ip_address", 15)
// TODO : Do we need to store this in column? or is it ok with blob.
val request = blob("request")
val requestTimestamp = localDateTime("request_timestamp")
val approvedTimestamp = localDateTime("approved_timestamp").nullable()
val certificate = blob("certificate").nullable()
}
init {
// Create table if not exists.
databaseTransaction(database) {
SchemaUtils.create(DataTable)
}
}
override fun getCertificate(requestId: String): Certificate? {
return databaseTransaction(database) { DataTable.select { DataTable.requestId.eq(requestId) }.map { it[DataTable.certificate] }.filterNotNull().map { deserializeFromBlob<Certificate>(it) }.firstOrNull() }
}
override fun saveCertificate(requestId: String, certificateGenerator: (CertificationData) -> Certificate) {
databaseTransaction(database) {
val finalizables = mutableListOf<() -> Unit>()
try {
getRequest(requestId)?.let {
val clientCert = certificateGenerator(it)
DataTable.update({ DataTable.requestId eq requestId }) {
it[approvedTimestamp] = LocalDateTime.now()
it[certificate] = serializeToBlob(clientCert, finalizables)
}
}
} finally {
finalizables.forEach { it() }
}
}
}
override fun getRequest(requestId: String): CertificationData? {
return databaseTransaction(database) { DataTable.select { DataTable.requestId eq requestId }.map { CertificationData(it[DataTable.hostName], it[DataTable.ipAddress], deserializeFromBlob(it[DataTable.request])) }.firstOrNull() }
}
override fun saveRequest(certificationData: CertificationData): String {
return databaseTransaction(database) {
val finalizables = mutableListOf<() -> Unit>()
try {
val requestId = SecureHash.randomSHA256().toString()
DataTable.insert {
it[DataTable.requestId] = requestId
it[hostName] = certificationData.hostName
it[ipAddress] = certificationData.ipAddr
it[DataTable.request] = serializeToBlob(certificationData.request, finalizables)
it[requestTimestamp] = LocalDateTime.now()
}
requestId
} finally {
finalizables.forEach { it() }
}
}
}
override fun pendingRequestIds(): List<String> {
return databaseTransaction(database) { DataTable.select { DataTable.approvedTimestamp.isNull() }.map { it[DataTable.requestId] } }
}
}

View File

@ -5,14 +5,24 @@ import java.security.cert.Certificate
import java.util.* import java.util.*
class InMemoryCertificationRequestStorage : CertificationRequestStorage { class InMemoryCertificationRequestStorage : CertificationRequestStorage {
val requestStore = HashMap<String, CertificationData>() private val requestStore = HashMap<String, CertificationData>()
val certificateStore = HashMap<String, Certificate>() private val certificateStore = HashMap<String, Certificate>()
override fun getOrElseCreateCertificate(requestId: String, certificateGenerator: () -> Certificate): Certificate { override fun pendingRequestIds(): List<String> {
return certificateStore.getOrPut(requestId, certificateGenerator) return requestStore.keys.filter { !certificateStore.keys.contains(it) }
} }
override fun getApprovedRequest(requestId: String): CertificationData? { override fun getCertificate(requestId: String): Certificate? {
return certificateStore[requestId]
}
override fun saveCertificate(requestId: String, certificateGenerator: (CertificationData) -> Certificate) {
requestStore[requestId]?.let {
certificateStore.putIfAbsent(requestId, certificateGenerator(it))
}
}
override fun getRequest(requestId: String): CertificationData? {
return requestStore[requestId] return requestStore[requestId]
} }

View File

@ -0,0 +1,14 @@
host = localhost
port = 0
keystorePath = ${basedir}"/certificates/keystore.jks"
keyStorePassword = "password"
caKeyPassword = "password"
approveAll = true
dataSourceProperties {
dataSourceClassName = org.h2.jdbcx.JdbcDataSource
"dataSource.url" = "jdbc:h2:file:"${basedir}"/persistence;DB_CLOSE_ON_EXIT=FALSE;LOCK_TIMEOUT=10000;MVCC=true;MV_STORE=true;WRITE_DELAY=0;AUTO_SERVER_PORT="${h2port}
"dataSource.user" = sa
"dataSource.password" = ""
}
h2port = 0

View File

@ -4,11 +4,11 @@ import com.google.common.net.HostAndPort
import com.nhaarman.mockito_kotlin.* import com.nhaarman.mockito_kotlin.*
import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.crypto.X509Utilities import com.r3corda.core.crypto.X509Utilities
import com.r3corda.core.seconds
import com.r3corda.netpermission.CertificateSigningServer.Companion.hostAndPort import com.r3corda.netpermission.CertificateSigningServer.Companion.hostAndPort
import com.r3corda.netpermission.internal.CertificateSigningService import com.r3corda.netpermission.internal.CertificateSigningService
import com.r3corda.netpermission.internal.persistence.CertificationData import com.r3corda.netpermission.internal.persistence.CertificationData
import com.r3corda.netpermission.internal.persistence.CertificationRequestStorage import com.r3corda.netpermission.internal.persistence.CertificationRequestStorage
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
import org.junit.Test import org.junit.Test
import sun.security.x509.X500Name import sun.security.x509.X500Name
import java.io.IOException import java.io.IOException
@ -20,11 +20,14 @@ import java.security.cert.X509Certificate
import java.util.* import java.util.*
import java.util.zip.ZipInputStream import java.util.zip.ZipInputStream
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
class CertificateSigningServiceTest { class CertificateSigningServiceTest {
private fun getSigningServer(storage: CertificationRequestStorage): CertificateSigningServer {
val rootCA = X509Utilities.createSelfSignedCACert("Corda Node Root CA") val rootCA = X509Utilities.createSelfSignedCACert("Corda Node Root CA")
val intermediateCA = X509Utilities.createSelfSignedCACert("Corda Node Intermediate CA") val intermediateCA = X509Utilities.createSelfSignedCACert("Corda Node Intermediate CA")
private fun getSigningServer(storage: CertificationRequestStorage): CertificateSigningServer {
return CertificateSigningServer(HostAndPort.fromParts("localhost", 0), CertificateSigningService(intermediateCA, rootCA.certificate, storage)) return CertificateSigningServer(HostAndPort.fromParts("localhost", 0), CertificateSigningService(intermediateCA, rootCA.certificate, storage))
} }
@ -60,13 +63,18 @@ class CertificateSigningServiceTest {
fun testRetrieveCertificate() { fun testRetrieveCertificate() {
val keyPair = X509Utilities.generateECDSAKeyPairForSSL() val keyPair = X509Utilities.generateECDSAKeyPairForSSL()
val id = SecureHash.randomSHA256().toString() val id = SecureHash.randomSHA256().toString()
var count = 0
// Mock Storage behaviour.
val certificateStore = mutableMapOf<String, Certificate>()
val storage: CertificationRequestStorage = mock { val storage: CertificationRequestStorage = mock {
on { getApprovedRequest(eq(id)) }.then { on { getCertificate(eq(id)) }.then { certificateStore[id] }
if (count < 5) null else CertificationData("", "", X509Utilities.createCertificateSigningRequest("LegalName", on { saveCertificate(eq(id), any()) }.then {
"London", "admin@test.com", keyPair)) val certGen = it.arguments[1] as (CertificationData) -> Certificate
val request = CertificationData("", "", X509Utilities.createCertificateSigningRequest("LegalName", "London", "admin@test.com", keyPair))
certificateStore[id] = certGen(request)
Unit
} }
on { getOrElseCreateCertificate(eq(id), any()) }.thenAnswer { (it.arguments[1] as () -> Certificate)() } on { pendingRequestIds() }.then { listOf(id) }
} }
getSigningServer(storage).use { getSigningServer(storage).use {
@ -91,15 +99,20 @@ class CertificateSigningServiceTest {
} }
} }
var certificates = poll() assertNull(poll())
assertNull(poll())
while (certificates == null) { storage.saveCertificate(id, {
Thread.sleep(1.seconds.toMillis()) JcaPKCS10CertificationRequest(it.request).run {
count++ X509Utilities.createServerCert(subject, publicKey, intermediateCA,
certificates = poll() if (it.ipAddr == it.hostName) listOf() else listOf(it.hostName), listOf(it.ipAddr))
} }
})
val certificates = assertNotNull(poll())
verify(storage, times(3)).getCertificate(any())
verify(storage, times(6)).getApprovedRequest(any())
assertEquals(3, certificates.size) assertEquals(3, certificates.size)
(certificates.first() as X509Certificate).run { (certificates.first() as X509Certificate).run {

View File

@ -0,0 +1,81 @@
package com.r3corda.netpermission.internal.persistence
import com.r3corda.core.crypto.X509Utilities
import com.r3corda.node.utilities.configureDatabase
import com.r3corda.testing.node.makeTestDataSourceProperties
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue
class DBCertificateRequestStorageTest {
val intermediateCA = X509Utilities.createSelfSignedCACert("Corda Node Intermediate CA")
@Test
fun testSaveRequest() {
val keyPair = X509Utilities.generateECDSAKeyPairForSSL()
val request = CertificationData("", "", X509Utilities.createCertificateSigningRequest("LegalName", "London", "admin@test.com", keyPair))
val (connection, db) = configureDatabase(makeTestDataSourceProperties())
connection.use {
val storage = DBCertificateRequestStorage(db)
val requestId = storage.saveRequest(request)
assertNotNull(storage.getRequest(requestId)).apply {
assertEquals(request.hostName, hostName)
assertEquals(request.ipAddr, ipAddr)
assertEquals(request.request, this.request)
}
}
}
@Test
fun testPendingRequest() {
val keyPair = X509Utilities.generateECDSAKeyPairForSSL()
val request = CertificationData("", "", X509Utilities.createCertificateSigningRequest("LegalName", "London", "admin@test.com", keyPair))
val (connection, db) = configureDatabase(makeTestDataSourceProperties())
connection.use {
val storage = DBCertificateRequestStorage(db)
val requestId = storage.saveRequest(request)
storage.pendingRequestIds().apply {
assertTrue(isNotEmpty())
assertEquals(1, size)
assertEquals(requestId, first())
}
}
}
@Test
fun testSaveCertificate() {
val keyPair = X509Utilities.generateECDSAKeyPairForSSL()
val request = CertificationData("", "", X509Utilities.createCertificateSigningRequest("LegalName", "London", "admin@test.com", keyPair))
val (connection, db) = configureDatabase(makeTestDataSourceProperties())
connection.use {
val storage = DBCertificateRequestStorage(db)
// Add request to DB.
val requestId = storage.saveRequest(request)
// Pending request should equals to 1.
assertEquals(1, storage.pendingRequestIds().size)
// Certificate should be empty.
assertNull(storage.getCertificate(requestId))
// Store certificate to DB.
storage.saveCertificate(requestId, {
JcaPKCS10CertificationRequest(it.request).run {
X509Utilities.createServerCert(subject, publicKey, intermediateCA,
if (it.ipAddr == it.hostName) listOf() else listOf(it.hostName), listOf(it.ipAddr))
}
})
// Check certificate is stored in DB correctly.
assertNotNull(storage.getCertificate(requestId)).apply {
assertEquals(keyPair.public, this.publicKey)
}
// Pending request should be empty.
assertTrue(storage.pendingRequestIds().isEmpty())
}
}
}