mirror of
https://github.com/corda/corda.git
synced 2025-01-04 04:04:27 +00:00
New module containing Certificate signing server for signing Corda node SSL certificate
This commit is contained in:
parent
a5774adba1
commit
5202def6d9
8
.idea/modules.xml
generated
8
.idea/modules.xml
generated
@ -24,9 +24,15 @@
|
|||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated.iml" group="contracts/isolated" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated.iml" group="contracts/isolated" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated_main.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated_main.iml" group="contracts/isolated" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated_main.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated_main.iml" group="contracts/isolated" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated_test.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated_test.iml" group="contracts/isolated" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated_test.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated_test.iml" group="contracts/isolated" />
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/netPermission/netpermission.iml" filepath="$PROJECT_DIR$/.idea/modules/netPermission/netpermission.iml" group="netpermission" />
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/netPermission/netpermission_main.iml" filepath="$PROJECT_DIR$/.idea/modules/netPermission/netpermission_main.iml" group="netpermission" />
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/netPermission/netpermission_test.iml" filepath="$PROJECT_DIR$/.idea/modules/netPermission/netpermission_test.iml" group="netpermission" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/network-explorer/network-explorer_main.iml" filepath="$PROJECT_DIR$/.idea/modules/network-explorer/network-explorer_main.iml" group="network-explorer" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/network-explorer/network-explorer_main.iml" filepath="$PROJECT_DIR$/.idea/modules/network-explorer/network-explorer_main.iml" group="network-explorer" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/network-explorer/network-explorer_test.iml" filepath="$PROJECT_DIR$/.idea/modules/network-explorer/network-explorer_test.iml" group="network-explorer" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/network-explorer/network-explorer_test.iml" filepath="$PROJECT_DIR$/.idea/modules/network-explorer/network-explorer_test.iml" group="network-explorer" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/network-explorer/network-map-visualiser.iml" filepath="$PROJECT_DIR$/.idea/modules/network-explorer/network-map-visualiser.iml" group="network-explorer" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/network-explorer/network-map-visualiser.iml" filepath="$PROJECT_DIR$/.idea/modules/network-explorer/network-map-visualiser.iml" group="network-explorer" />
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/network-simulator/network-simulator.iml" filepath="$PROJECT_DIR$/.idea/modules/network-simulator/network-simulator.iml" group="network-simulator" />
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/network-simulator/network-simulator_main.iml" filepath="$PROJECT_DIR$/.idea/modules/network-simulator/network-simulator_main.iml" group="network-simulator" />
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/network-simulator/network-simulator_test.iml" filepath="$PROJECT_DIR$/.idea/modules/network-simulator/network-simulator_test.iml" group="network-simulator" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/node/node.iml" filepath="$PROJECT_DIR$/.idea/modules/node/node.iml" group="node" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/node/node.iml" filepath="$PROJECT_DIR$/.idea/modules/node/node.iml" group="node" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/node/node_integrationTest.iml" filepath="$PROJECT_DIR$/.idea/modules/node/node_integrationTest.iml" group="node" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/node/node_integrationTest.iml" filepath="$PROJECT_DIR$/.idea/modules/node/node_integrationTest.iml" group="node" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/node/node_main.iml" filepath="$PROJECT_DIR$/.idea/modules/node/node_main.iml" group="node" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/node/node_main.iml" filepath="$PROJECT_DIR$/.idea/modules/node/node_main.iml" group="node" />
|
||||||
@ -40,4 +46,4 @@
|
|||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/test-utils/test-utils_test.iml" filepath="$PROJECT_DIR$/.idea/modules/test-utils/test-utils_test.iml" group="test-utils" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/test-utils/test-utils_test.iml" filepath="$PROJECT_DIR$/.idea/modules/test-utils/test-utils_test.iml" group="test-utils" />
|
||||||
</modules>
|
</modules>
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
53
netpermission/build.gradle
Normal file
53
netpermission/build.gradle
Normal file
@ -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"
|
||||||
|
}
|
108
netpermission/src/main/kotlin/com/r3corda/netpermission/Main.kt
Normal file
108
netpermission/src/main/kotlin/com/r3corda/netpermission/Main.kt
Normal file
@ -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<CertificateSigningServer>()
|
||||||
|
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<String>) {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
@ -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<String, CertificationData>()
|
||||||
|
val certificateStore = HashMap<String, Certificate>()
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
@ -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<Certificate>()
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -7,4 +7,5 @@ include 'client'
|
|||||||
include 'experimental'
|
include 'experimental'
|
||||||
include 'test-utils'
|
include 'test-utils'
|
||||||
include 'network-simulator'
|
include 'network-simulator'
|
||||||
|
include 'netpermission'
|
||||||
include 'explorer'
|
include 'explorer'
|
||||||
|
Loading…
Reference in New Issue
Block a user