diff --git a/netpermission/build.gradle b/netpermission/build.gradle index c712f3ece4..d48d0242eb 100644 --- a/netpermission/build.gradle +++ b/netpermission/build.gradle @@ -23,10 +23,25 @@ task buildCertSignerJAR(type: FatCapsule, dependsOn: 'jar') { } } +sourceSets { + main { + resources { + srcDir "../config/dev" + } + } + test { + resources { + srcDir "../config/test" + } + } +} + dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" compile project(":core") + compile project(":node") + testCompile project(":test-utils") // Log4J: logging framework (with SLF4J bindings) compile "org.apache.logging.log4j:log4j-slf4j-impl:${log4j_version}" @@ -46,6 +61,9 @@ dependencies { // 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}" diff --git a/netpermission/src/main/kotlin/com/r3corda/netpermission/Main.kt b/netpermission/src/main/kotlin/com/r3corda/netpermission/Main.kt index 9771201f81..540aff3b5e 100644 --- a/netpermission/src/main/kotlin/com/r3corda/netpermission/Main.kt +++ b/netpermission/src/main/kotlin/com/r3corda/netpermission/Main.kt @@ -2,11 +2,16 @@ package com.r3corda.netpermission import com.google.common.net.HostAndPort 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.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 org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest import org.eclipse.jetty.server.Server import org.eclipse.jetty.server.ServerConnector import org.eclipse.jetty.server.handler.HandlerCollection @@ -17,6 +22,7 @@ import org.glassfish.jersey.servlet.ServletContainer import java.io.Closeable import java.net.InetSocketAddress import java.nio.file.Paths +import kotlin.concurrent.thread import kotlin.system.exitProcess /** @@ -68,20 +74,11 @@ class CertificateSigningServer(val webServerAddr: HostAndPort, val certSigningSe object ParamsSpec { val parser = OptionParser() - val host = parser.accepts("host", "The hostname permissioning server will be running on.") - .withRequiredArg().defaultsTo("localhost") - 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() + val basedir: ArgumentAcceptingOptionSpec? = parser.accepts("basedir", "Overriding configuration file path.") + .withRequiredArg() } fun main(args: Array) { - LogHelper.setLevel(CertificateSigningServer::class) val log = CertificateSigningServer.log log.info("Starting certificate signing server.") try { @@ -91,17 +88,48 @@ fun main(args: Array) { ParamsSpec.parser.printHelpOn(System.out) exitProcess(1) }.run { - // Load keystore from input path, default to Dev keystore from jar resource if path not defined. - val storePassword = valueOf(ParamsSpec.storePassword) - val keyPassword = valueOf(ParamsSpec.caKeyPassword) - val keystore = X509Utilities.loadKeyStore(Paths.get(valueOf(ParamsSpec.keystorePath)).normalize(), storePassword) - val intermediateCACertAndKey = X509Utilities.loadCertificateAndKey(keystore, keyPassword, X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY) + val basedir = Paths.get(valueOf(ParamsSpec.basedir) ?: ".") + val config = ConfigHelper.loadConfig(basedir) + + val keystore = X509Utilities.loadKeyStore(Paths.get(config.getString("keystorePath")).normalize(), config.getString("keyStorePassword")) + 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() - // TODO: Create a proper request storage using database or other storage technology. - val service = CertificateSigningService(intermediateCACertAndKey, rootCA, InMemoryCertificationRequestStorage()) + // Create DB connection. + 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() } } diff --git a/netpermission/src/main/kotlin/com/r3corda/netpermission/internal/CertificateSigningService.kt b/netpermission/src/main/kotlin/com/r3corda/netpermission/internal/CertificateSigningService.kt index d83b86ae93..9d34e61836 100644 --- a/netpermission/src/main/kotlin/com/r3corda/netpermission/internal/CertificateSigningService.kt +++ b/netpermission/src/main/kotlin/com/r3corda/netpermission/internal/CertificateSigningService.kt @@ -53,14 +53,8 @@ class CertificateSigningService(val intermediateCACertAndKey: X509Utilities.CACe @Path("certificate/{var}") @Produces(MediaType.APPLICATION_OCTET_STREAM) fun retrieveCert(@PathParam("var") requestId: String): Response { - val data = storage.getApprovedRequest(requestId) - return if (data != 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)) - } - } + val clientCert = storage.getCertificate(requestId) + return if (clientCert != null) { // Write certificate chain to a zip stream and extract the bit array output. ByteArrayOutputStream().use { ZipOutputStream(it).use { diff --git a/netpermission/src/main/kotlin/com/r3corda/netpermission/internal/persistence/CertificationRequestStorage.kt b/netpermission/src/main/kotlin/com/r3corda/netpermission/internal/persistence/CertificationRequestStorage.kt index f9b0034441..83c13a6240 100644 --- a/netpermission/src/main/kotlin/com/r3corda/netpermission/internal/persistence/CertificationRequestStorage.kt +++ b/netpermission/src/main/kotlin/com/r3corda/netpermission/internal/persistence/CertificationRequestStorage.kt @@ -2,6 +2,7 @@ package com.r3corda.netpermission.internal.persistence import org.bouncycastle.pkcs.PKCS10CertificationRequest import java.security.cert.Certificate + /** * Provide certificate signing request storage for the certificate signing server. */ @@ -12,16 +13,25 @@ interface CertificationRequestStorage { fun saveRequest(certificationData: CertificationData): String /** - * Retrieve approved certificate singing request and Host/IP information using [requestId]. - * Returns [CertificationData] if request has been approved, else returns null. + * Retrieve certificate singing request and Host/IP information using [requestId]. */ - fun getApprovedRequest(requestId: String): CertificationData? + fun getRequest(requestId: String): CertificationData? /** * 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 used for the background thread to approve request automatically without KYC checks, should be removed after testnet. + */ + fun pendingRequestIds(): List } data class CertificationData(val hostName: String, val ipAddr: String, val request: PKCS10CertificationRequest) \ No newline at end of file diff --git a/netpermission/src/main/kotlin/com/r3corda/netpermission/internal/persistence/DBCertificateRequestStorage.kt b/netpermission/src/main/kotlin/com/r3corda/netpermission/internal/persistence/DBCertificateRequestStorage.kt new file mode 100644 index 0000000000..48675ff395 --- /dev/null +++ b/netpermission/src/main/kotlin/com/r3corda/netpermission/internal/persistence/DBCertificateRequestStorage.kt @@ -0,0 +1,69 @@ +package com.r3corda.netpermission.internal.persistence + +import com.r3corda.core.crypto.SecureHash +import com.r3corda.node.utilities.* +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(it) }.firstOrNull() } + } + + override fun saveCertificate(requestId: String, certificateGenerator: (CertificationData) -> Certificate) { + databaseTransaction(database) { + withFinalizables { finalizables -> + getRequest(requestId)?.let { + val clientCert = certificateGenerator(it) + DataTable.update({ DataTable.requestId eq requestId }) { + it[approvedTimestamp] = LocalDateTime.now() + it[certificate] = serializeToBlob(clientCert, finalizables) + } + } + } + } + } + + 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) { + withFinalizables { finalizables -> + 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 + } + } + } + + override fun pendingRequestIds(): List { + return databaseTransaction(database) { DataTable.select { DataTable.approvedTimestamp.isNull() }.map { it[DataTable.requestId] } } + } +} \ No newline at end of file diff --git a/netpermission/src/main/kotlin/com/r3corda/netpermission/internal/persistence/InMemoryCertificationRequestStorage.kt b/netpermission/src/main/kotlin/com/r3corda/netpermission/internal/persistence/InMemoryCertificationRequestStorage.kt index cc49d2c689..91e71c6933 100644 --- a/netpermission/src/main/kotlin/com/r3corda/netpermission/internal/persistence/InMemoryCertificationRequestStorage.kt +++ b/netpermission/src/main/kotlin/com/r3corda/netpermission/internal/persistence/InMemoryCertificationRequestStorage.kt @@ -5,14 +5,24 @@ import java.security.cert.Certificate import java.util.* class InMemoryCertificationRequestStorage : CertificationRequestStorage { - val requestStore = HashMap() - val certificateStore = HashMap() + private val requestStore = HashMap() + private val certificateStore = HashMap() - override fun getOrElseCreateCertificate(requestId: String, certificateGenerator: () -> Certificate): Certificate { - return certificateStore.getOrPut(requestId, certificateGenerator) + override fun pendingRequestIds(): List { + 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] } diff --git a/netpermission/src/main/resources/reference.conf b/netpermission/src/main/resources/reference.conf new file mode 100644 index 0000000000..d0629fc739 --- /dev/null +++ b/netpermission/src/main/resources/reference.conf @@ -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;WRITE_DELAY=0;AUTO_SERVER_PORT="${h2port} + "dataSource.user" = sa + "dataSource.password" = "" +} +h2port = 0 \ No newline at end of file diff --git a/netpermission/src/test/kotlin/com/r3corda/netpermission/CertificateSigningServiceTest.kt b/netpermission/src/test/kotlin/com/r3corda/netpermission/CertificateSigningServiceTest.kt index e8ccb4f3bb..9389739e25 100644 --- a/netpermission/src/test/kotlin/com/r3corda/netpermission/CertificateSigningServiceTest.kt +++ b/netpermission/src/test/kotlin/com/r3corda/netpermission/CertificateSigningServiceTest.kt @@ -4,11 +4,11 @@ import com.google.common.net.HostAndPort import com.nhaarman.mockito_kotlin.* import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.X509Utilities -import com.r3corda.core.seconds import com.r3corda.netpermission.CertificateSigningServer.Companion.hostAndPort import com.r3corda.netpermission.internal.CertificateSigningService import com.r3corda.netpermission.internal.persistence.CertificationData import com.r3corda.netpermission.internal.persistence.CertificationRequestStorage +import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest import org.junit.Test import sun.security.x509.X500Name import java.io.IOException @@ -20,16 +20,19 @@ import java.security.cert.X509Certificate import java.util.* import java.util.zip.ZipInputStream import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull class CertificateSigningServiceTest { + val rootCA = X509Utilities.createSelfSignedCACert("Corda Node Root CA") + val intermediateCA = X509Utilities.createSelfSignedCACert("Corda Node Intermediate CA") + private fun getSigningServer(storage: CertificationRequestStorage): CertificateSigningServer { - val rootCA = X509Utilities.createSelfSignedCACert("Corda Node Root CA") - val intermediateCA = X509Utilities.createSelfSignedCACert("Corda Node Intermediate CA") return CertificateSigningServer(HostAndPort.fromParts("localhost", 0), CertificateSigningService(intermediateCA, rootCA.certificate, storage)) } @Test - fun testSubmitRequest() { + fun `test submit request`() { val id = SecureHash.randomSHA256().toString() val storage: CertificationRequestStorage = mock { @@ -57,16 +60,21 @@ class CertificateSigningServiceTest { } @Test - fun testRetrieveCertificate() { + fun `test retrieve certificate`() { val keyPair = X509Utilities.generateECDSAKeyPairForSSL() val id = SecureHash.randomSHA256().toString() - var count = 0 + + // Mock Storage behaviour. + val certificateStore = mutableMapOf() val storage: CertificationRequestStorage = mock { - on { getApprovedRequest(eq(id)) }.then { - if (count < 5) null else CertificationData("", "", X509Utilities.createCertificateSigningRequest("LegalName", - "London", "admin@test.com", keyPair)) + on { getCertificate(eq(id)) }.then { certificateStore[id] } + on { saveCertificate(eq(id), any()) }.then { + 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 { @@ -91,15 +99,18 @@ class CertificateSigningServiceTest { } } - var certificates = poll() + assertNull(poll()) + assertNull(poll()) - while (certificates == null) { - Thread.sleep(1.seconds.toMillis()) - count++ - certificates = poll() - } + storage.saveCertificate(id, { + JcaPKCS10CertificationRequest(it.request).run { + X509Utilities.createServerCert(subject, publicKey, intermediateCA, + if (it.ipAddr == it.hostName) listOf() else listOf(it.hostName), listOf(it.ipAddr)) + } + }) - verify(storage, times(6)).getApprovedRequest(any()) + val certificates = assertNotNull(poll()) + verify(storage, times(3)).getCertificate(any()) assertEquals(3, certificates.size) (certificates.first() as X509Certificate).run { diff --git a/netpermission/src/test/kotlin/com/r3corda/netpermission/internal/persistence/DBCertificateRequestStorageTest.kt b/netpermission/src/test/kotlin/com/r3corda/netpermission/internal/persistence/DBCertificateRequestStorageTest.kt new file mode 100644 index 0000000000..454c3695f4 --- /dev/null +++ b/netpermission/src/test/kotlin/com/r3corda/netpermission/internal/persistence/DBCertificateRequestStorageTest.kt @@ -0,0 +1,80 @@ +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 `test save request`() { + 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 `test pending request`() { + 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 `test save certificate`() { + 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()) + } + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/com/r3corda/node/utilities/DatabaseSupport.kt b/node/src/main/kotlin/com/r3corda/node/utilities/DatabaseSupport.kt index 134fe0e498..990ed19375 100644 --- a/node/src/main/kotlin/com/r3corda/node/utilities/DatabaseSupport.kt +++ b/node/src/main/kotlin/com/r3corda/node/utilities/DatabaseSupport.kt @@ -30,6 +30,18 @@ fun databaseTransaction(db: Database, statement: Transaction.() -> T): T { return org.jetbrains.exposed.sql.transactions.transaction(Connection.TRANSACTION_REPEATABLE_READ, 1, statement) } +/** + * Helper method wrapping code in try finally block. A mutable list is used to keep track of functions that need to be executed in finally block. + */ +fun withFinalizables(statement: (MutableList<() -> Unit>) -> T): T { + val finalizables = mutableListOf<() -> Unit>() + return try { + statement(finalizables) + } finally { + finalizables.forEach { it() } + } +} + fun createDatabaseTransaction(db: Database): Transaction { // We need to set the database for the current [Thread] or [Fiber] here as some tests share threads across databases. StrandLocalTransactionManager.database = db