diff --git a/.idea/modules.xml b/.idea/modules.xml
index ceb764a238..4f2556bdc3 100644
--- a/.idea/modules.xml
+++ b/.idea/modules.xml
@@ -24,9 +24,15 @@
+
+
+
+
+
+
@@ -40,4 +46,4 @@
-
+
\ No newline at end of file
diff --git a/netpermission/build.gradle b/netpermission/build.gradle
new file mode 100644
index 0000000000..c712f3ece4
--- /dev/null
+++ b/netpermission/build.gradle
@@ -0,0 +1,53 @@
+apply plugin: 'us.kirchmeier.capsule'
+apply plugin: 'kotlin'
+
+repositories {
+ mavenLocal()
+ mavenCentral()
+ maven {
+ url 'http://oss.sonatype.org/content/repositories/snapshots'
+ }
+ jcenter()
+ maven {
+ url 'https://dl.bintray.com/kotlin/exposed'
+ }
+}
+
+task buildCertSignerJAR(type: FatCapsule, dependsOn: 'jar') {
+ applicationClass 'com.r3corda.netpermission.MainKt'
+ archiveName 'certSigner.jar'
+
+ capsuleManifest {
+ systemProperties['log4j.configuration'] = 'log4j2.xml'
+ minJavaVersion = '1.8.0'
+ }
+}
+
+dependencies {
+ compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
+
+ compile project(":core")
+
+ // 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}"
+
+ // Web stuff: for HTTP[S] servlets
+ compile "org.eclipse.jetty:jetty-servlet:${jetty_version}"
+ compile "org.eclipse.jetty:jetty-webapp:${jetty_version}"
+ compile "javax.servlet:javax.servlet-api:3.1.0"
+
+ // Jersey for JAX-RS implementation for use in Jetty
+ compile "org.glassfish.jersey.core:jersey-server:${jersey_version}"
+ compile "org.glassfish.jersey.containers:jersey-container-servlet-core:${jersey_version}"
+ compile "org.glassfish.jersey.containers:jersey-container-jetty-http:${jersey_version}"
+
+ // JOpt: for command line flags.
+ compile "net.sf.jopt-simple:jopt-simple:5.0.2"
+
+ // Unit testing helpers.
+ testCompile 'junit:junit:4.12'
+ testCompile "org.assertj:assertj-core:${assertj_version}"
+ testCompile "com.nhaarman:mockito-kotlin:0.6.1"
+}
diff --git a/netpermission/src/main/kotlin/com/r3corda/netpermission/Main.kt b/netpermission/src/main/kotlin/com/r3corda/netpermission/Main.kt
new file mode 100644
index 0000000000..22308cc464
--- /dev/null
+++ b/netpermission/src/main/kotlin/com/r3corda/netpermission/Main.kt
@@ -0,0 +1,108 @@
+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.loggerFor
+import com.r3corda.netpermission.internal.CertificateSigningService
+import com.r3corda.netpermission.internal.persistence.InMemoryCertificationRequestStorage
+import joptsimple.OptionParser
+import org.eclipse.jetty.server.Server
+import org.eclipse.jetty.server.ServerConnector
+import org.eclipse.jetty.server.handler.HandlerCollection
+import org.eclipse.jetty.servlet.ServletContextHandler
+import org.eclipse.jetty.servlet.ServletHolder
+import org.glassfish.jersey.server.ResourceConfig
+import org.glassfish.jersey.servlet.ServletContainer
+import java.io.Closeable
+import java.net.InetSocketAddress
+import java.nio.file.Paths
+import kotlin.system.exitProcess
+
+/**
+ * CertificateSigningServer runs on Jetty server and provide certificate signing service via http.
+ * The server will require keystorePath, keystore password and key password via command line input.
+ * 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 {
+ companion object {
+ val log = loggerFor()
+ fun Server.hostAndPort(): HostAndPort {
+ val connector = server.connectors.first() as ServerConnector
+ return HostAndPort.fromParts(connector.host, connector.localPort)
+ }
+ }
+
+ val server: Server = initWebServer()
+
+ override fun close() {
+ log.info("Shutting down CertificateSigningService...")
+ server.stop()
+ }
+
+ private fun initWebServer(): Server {
+ return Server(InetSocketAddress(webServerAddr.hostText, webServerAddr.port)).apply {
+ log.info("Starting CertificateSigningService...")
+ handler = HandlerCollection().apply {
+ addHandler(buildServletContextHandler())
+ }
+ start()
+ log.info("CertificateSigningService started on ${server.hostAndPort()}")
+ }
+ }
+
+ private fun buildServletContextHandler(): ServletContextHandler {
+ return ServletContextHandler().apply {
+ contextPath = "/"
+ val resourceConfig = ResourceConfig().apply {
+ // Add your API provider classes (annotated for JAX-RS) here
+ register(certSigningService)
+ }
+ val jerseyServlet = ServletHolder(ServletContainer(resourceConfig)).apply {
+ initOrder = 0 // Initialise at server start
+ }
+ addServlet(jerseyServlet, "/api/*")
+ }
+ }
+}
+
+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()
+}
+
+fun main(args: Array) {
+ LogHelper.setLevel(CertificateSigningServer::class)
+ val log = CertificateSigningServer.log
+ log.info("Starting certificate signing server.")
+ try {
+ ParamsSpec.parser.parse(*args)
+ } catch (ex: Exception) {
+ log.error("Unable to parse args", ex)
+ 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 rootCA = keystore.getCertificate(X509Utilities.CORDA_ROOT_CA_PRIVATE_KEY)
+
+ // TODO: Create a proper request storage using database or other storage technology.
+ val service = CertificateSigningService(intermediateCACertAndKey, rootCA, InMemoryCertificationRequestStorage())
+
+ CertificateSigningServer(HostAndPort.fromParts(valueOf(ParamsSpec.host), valueOf(ParamsSpec.port)), service).use {
+ it.server.join()
+ }
+ }
+}
\ No newline at end of file
diff --git a/netpermission/src/main/kotlin/com/r3corda/netpermission/internal/CertificateSigningService.kt b/netpermission/src/main/kotlin/com/r3corda/netpermission/internal/CertificateSigningService.kt
new file mode 100644
index 0000000000..d83b86ae93
--- /dev/null
+++ b/netpermission/src/main/kotlin/com/r3corda/netpermission/internal/CertificateSigningService.kt
@@ -0,0 +1,86 @@
+package com.r3corda.netpermission.internal
+
+import com.r3corda.core.crypto.X509Utilities
+import com.r3corda.core.crypto.X509Utilities.CORDA_CLIENT_CA
+import com.r3corda.core.crypto.X509Utilities.CORDA_INTERMEDIATE_CA
+import com.r3corda.core.crypto.X509Utilities.CORDA_ROOT_CA
+import com.r3corda.netpermission.internal.persistence.CertificationData
+import com.r3corda.netpermission.internal.persistence.CertificationRequestStorage
+import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
+import java.io.ByteArrayOutputStream
+import java.io.InputStream
+import java.security.cert.Certificate
+import java.util.zip.ZipEntry
+import java.util.zip.ZipOutputStream
+import javax.servlet.http.HttpServletRequest
+import javax.ws.rs.*
+import javax.ws.rs.core.Context
+import javax.ws.rs.core.MediaType
+import javax.ws.rs.core.Response
+import javax.ws.rs.core.Response.noContent
+import javax.ws.rs.core.Response.ok
+
+/**
+ * Provides functionality for asynchronous submission of certificate signing requests and retrieval of the results.
+ */
+@Path("")
+class CertificateSigningService(val intermediateCACertAndKey: X509Utilities.CACertAndKey, val rootCert: Certificate, val storage: CertificationRequestStorage) {
+ @Context lateinit var request: HttpServletRequest
+ /**
+ * Accept stream of [PKCS10CertificationRequest] from user and persists in [CertificationRequestStorage] for approval.
+ * Server returns HTTP 200 response with random generated request Id after request has been persisted.
+ */
+ @POST
+ @Path("certificate")
+ @Consumes(MediaType.APPLICATION_OCTET_STREAM)
+ @Produces(MediaType.TEXT_PLAIN)
+ fun submitRequest(input: InputStream): Response {
+ val certificationRequest = input.use {
+ JcaPKCS10CertificationRequest(it.readBytes())
+ }
+ // TODO: Certificate signing request verifications.
+ // TODO: Use jira api / slack bot to semi automate the approval process?
+ // TODO: Acknowledge to user we have received the request via email?
+ val requestId = storage.saveRequest(CertificationData(request.remoteHost, request.remoteAddr, certificationRequest))
+ return ok(requestId).build()
+ }
+
+ /**
+ * Retrieve Certificate signing request from storage using the [requestId] and create a signed certificate if request has been approved.
+ * Returns HTTP 200 with DER encoded signed certificates if request has been approved else HTTP 204 No content
+ */
+ @GET
+ @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))
+ }
+ }
+ // Write certificate chain to a zip stream and extract the bit array output.
+ ByteArrayOutputStream().use {
+ ZipOutputStream(it).use {
+ zipStream ->
+ // Client certificate must come first and root certificate should come last.
+ mapOf(CORDA_CLIENT_CA to clientCert,
+ CORDA_INTERMEDIATE_CA to intermediateCACertAndKey.certificate,
+ CORDA_ROOT_CA to rootCert).forEach {
+ zipStream.putNextEntry(ZipEntry("${it.key}.cer"))
+ zipStream.write(it.value.encoded)
+ zipStream.setComment(it.key)
+ zipStream.closeEntry()
+ }
+ }
+ ok(it.toByteArray())
+ .type("application/zip")
+ .header("Content-Disposition", "attachment; filename=\"certificates.zip\"")
+ }
+ } else {
+ noContent()
+ }.build()
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000000..f9b0034441
--- /dev/null
+++ b/netpermission/src/main/kotlin/com/r3corda/netpermission/internal/persistence/CertificationRequestStorage.kt
@@ -0,0 +1,27 @@
+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.
+ */
+interface CertificationRequestStorage {
+ /**
+ * Persist [certificationData] in storage for further approval, returns randomly generated request ID.
+ */
+ 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.
+ */
+ fun getApprovedRequest(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
+}
+
+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/InMemoryCertificationRequestStorage.kt b/netpermission/src/main/kotlin/com/r3corda/netpermission/internal/persistence/InMemoryCertificationRequestStorage.kt
new file mode 100644
index 0000000000..cc49d2c689
--- /dev/null
+++ b/netpermission/src/main/kotlin/com/r3corda/netpermission/internal/persistence/InMemoryCertificationRequestStorage.kt
@@ -0,0 +1,24 @@
+package com.r3corda.netpermission.internal.persistence
+
+import com.r3corda.core.crypto.SecureHash
+import java.security.cert.Certificate
+import java.util.*
+
+class InMemoryCertificationRequestStorage : CertificationRequestStorage {
+ val requestStore = HashMap()
+ val certificateStore = HashMap()
+
+ override fun getOrElseCreateCertificate(requestId: String, certificateGenerator: () -> Certificate): Certificate {
+ return certificateStore.getOrPut(requestId, certificateGenerator)
+ }
+
+ override fun getApprovedRequest(requestId: String): CertificationData? {
+ return requestStore[requestId]
+ }
+
+ override fun saveRequest(certificationData: CertificationData): String {
+ val requestId = SecureHash.randomSHA256().toString()
+ requestStore.put(requestId, certificationData)
+ return requestId
+ }
+}
\ 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
new file mode 100644
index 0000000000..e8ccb4f3bb
--- /dev/null
+++ b/netpermission/src/test/kotlin/com/r3corda/netpermission/CertificateSigningServiceTest.kt
@@ -0,0 +1,116 @@
+package com.r3corda.netpermission
+
+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.junit.Test
+import sun.security.x509.X500Name
+import java.io.IOException
+import java.net.HttpURLConnection
+import java.net.URL
+import java.security.cert.Certificate
+import java.security.cert.CertificateFactory
+import java.security.cert.X509Certificate
+import java.util.*
+import java.util.zip.ZipInputStream
+import kotlin.test.assertEquals
+
+class CertificateSigningServiceTest {
+ 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() {
+ val id = SecureHash.randomSHA256().toString()
+
+ val storage: CertificationRequestStorage = mock {
+ on { saveRequest(any()) }.then { id }
+ }
+
+ getSigningServer(storage).use {
+ val keyPair = X509Utilities.generateECDSAKeyPairForSSL()
+ val request = X509Utilities.createCertificateSigningRequest("Test", "London", "admin@test.com", keyPair)
+ // Post request to signing server via http.
+ val submitRequest = {
+ val conn = URL("http://${it.server.hostAndPort()}/api/certificate").openConnection() as HttpURLConnection
+ conn.doOutput = true
+ conn.requestMethod = "POST"
+ conn.setRequestProperty("Content-Type", "application/octet-stream")
+ conn.outputStream.write(request.encoded)
+ conn.inputStream.bufferedReader().readLine()
+ }
+
+ assertEquals(id, submitRequest())
+ verify(storage, times(1)).saveRequest(any())
+ submitRequest()
+ verify(storage, times(2)).saveRequest(any())
+ }
+ }
+
+ @Test
+ fun testRetrieveCertificate() {
+ val keyPair = X509Utilities.generateECDSAKeyPairForSSL()
+ val id = SecureHash.randomSHA256().toString()
+ var count = 0
+ val storage: CertificationRequestStorage = mock {
+ on { getApprovedRequest(eq(id)) }.then {
+ if (count < 5) null else CertificationData("", "", X509Utilities.createCertificateSigningRequest("LegalName",
+ "London", "admin@test.com", keyPair))
+ }
+ on { getOrElseCreateCertificate(eq(id), any()) }.thenAnswer { (it.arguments[1] as () -> Certificate)() }
+ }
+
+ getSigningServer(storage).use {
+ val poll = {
+ val url = URL("http://${it.server.hostAndPort()}/api/certificate/$id")
+ val conn = url.openConnection() as HttpURLConnection
+ conn.requestMethod = "GET"
+
+ when (conn.responseCode) {
+ HttpURLConnection.HTTP_OK -> conn.inputStream.use {
+ ZipInputStream(it).use {
+ val certificates = ArrayList()
+ while (it.nextEntry != null) {
+ certificates.add(CertificateFactory.getInstance("X.509").generateCertificate(it))
+ }
+ certificates
+ }
+ }
+ HttpURLConnection.HTTP_NO_CONTENT -> null
+ else ->
+ throw IOException("Cannot connect to Certificate Signing Server, HTTP response code : ${conn.responseCode}")
+ }
+ }
+
+ var certificates = poll()
+
+ while (certificates == null) {
+ Thread.sleep(1.seconds.toMillis())
+ count++
+ certificates = poll()
+ }
+
+ verify(storage, times(6)).getApprovedRequest(any())
+ assertEquals(3, certificates.size)
+
+ (certificates.first() as X509Certificate).run {
+ assertEquals("LegalName", (subjectDN as X500Name).commonName)
+ assertEquals("London", (subjectDN as X500Name).locality)
+ }
+
+ (certificates.last() as X509Certificate).run {
+ assertEquals("Corda Node Root CA", (subjectDN as X500Name).commonName)
+ assertEquals("London", (subjectDN as X500Name).locality)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index 5eb65bc579..76590d2a9a 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -7,4 +7,5 @@ include 'client'
include 'experimental'
include 'test-utils'
include 'network-simulator'
+include 'netpermission'
include 'explorer'