Adding new statuses for the CSR (#46)

* Adding new statuses for the CSR

* Adding test for signing

* Addressing review comments

* Addressing review comments

* Doorman refactoring

* Addressing review comments
This commit is contained in:
mkit 2017-09-25 13:38:39 +01:00 committed by GitHub
parent 8376f29cac
commit 34c0f6b89c
13 changed files with 478 additions and 268 deletions

View File

@ -17,14 +17,12 @@ data class DoormanParameters(val basedir: Path,
val host: String, val host: String,
val port: Int, val port: Int,
val dataSourceProperties: Properties, val dataSourceProperties: Properties,
val mode: Mode,
val databaseProperties: Properties? = null, val databaseProperties: Properties? = null,
val keygen: Boolean = false,
val rootKeygen: Boolean = false,
val jiraConfig: JiraConfig? = null, val jiraConfig: JiraConfig? = null,
val keystorePath: Path = basedir / "certificates" / "caKeystore.jks", val keystorePath: Path? = null, // basedir / "certificates" / "caKeystore.jks",
val rootStorePath: Path = basedir / "certificates" / "rootCAKeystore.jks" val rootStorePath: Path? = null // basedir / "certificates" / "rootCAKeystore.jks"
) { ) {
val mode = if (rootKeygen) Mode.ROOT_KEYGEN else if (keygen) Mode.CA_KEYGEN else Mode.DOORMAN
enum class Mode { enum class Mode {
DOORMAN, CA_KEYGEN, ROOT_KEYGEN DOORMAN, CA_KEYGEN, ROOT_KEYGEN
@ -43,10 +41,9 @@ fun parseParameters(vararg args: String): DoormanParameters {
val argConfig = args.toConfigWithOptions { val argConfig = args.toConfigWithOptions {
accepts("basedir", "Overriding configuration filepath, default to current directory.").withRequiredArg().defaultsTo(".").describedAs("filepath") accepts("basedir", "Overriding configuration filepath, default to current directory.").withRequiredArg().defaultsTo(".").describedAs("filepath")
accepts("configFile", "Overriding configuration file, default to <<current directory>>/node.conf.").withRequiredArg().describedAs("filepath") accepts("configFile", "Overriding configuration file, default to <<current directory>>/node.conf.").withRequiredArg().describedAs("filepath")
accepts("keygen", "Generate CA keypair and certificate using provide Root CA key.").withOptionalArg() accepts("mode", "Execution mode. Allowed values: ${DoormanParameters.Mode.values()}").withRequiredArg().defaultsTo(DoormanParameters.Mode.DOORMAN.name)
accepts("rootKeygen", "Generate Root CA keypair and certificate.").withOptionalArg() accepts("keystorePath", "CA keystore filepath").withRequiredArg().describedAs("filepath")
accepts("keystorePath", "CA keystore filepath, default to [basedir]/certificates/caKeystore.jks.").withRequiredArg().describedAs("filepath") accepts("rootStorePath", "Root CA keystore filepath").withRequiredArg().describedAs("filepath")
accepts("rootStorePath", "Root CA keystore filepath, default to [basedir]/certificates/rootCAKeystore.jks.").withRequiredArg().describedAs("filepath")
accepts("keystorePassword", "CA keystore password.").withRequiredArg().describedAs("password") accepts("keystorePassword", "CA keystore password.").withRequiredArg().describedAs("password")
accepts("caPrivateKeyPassword", "CA private key password.").withRequiredArg().describedAs("password") accepts("caPrivateKeyPassword", "CA private key password.").withRequiredArg().describedAs("password")
accepts("rootKeystorePassword", "Root CA keystore password.").withRequiredArg().describedAs("password") accepts("rootKeystorePassword", "Root CA keystore password.").withRequiredArg().describedAs("password")

View File

@ -3,7 +3,7 @@ package com.r3.corda.doorman
import com.r3.corda.doorman.persistence.CertificateResponse import com.r3.corda.doorman.persistence.CertificateResponse
import com.r3.corda.doorman.persistence.CertificationRequestData import com.r3.corda.doorman.persistence.CertificationRequestData
import com.r3.corda.doorman.persistence.CertificationRequestStorage import com.r3.corda.doorman.persistence.CertificationRequestStorage
import net.corda.node.utilities.CertificateAndKeyPair import com.r3.corda.doorman.signer.DefaultCsrHandler
import net.corda.node.utilities.X509Utilities.CORDA_CLIENT_CA import net.corda.node.utilities.X509Utilities.CORDA_CLIENT_CA
import net.corda.node.utilities.X509Utilities.CORDA_INTERMEDIATE_CA import net.corda.node.utilities.X509Utilities.CORDA_INTERMEDIATE_CA
import net.corda.node.utilities.X509Utilities.CORDA_ROOT_CA import net.corda.node.utilities.X509Utilities.CORDA_ROOT_CA
@ -11,7 +11,6 @@ import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
import org.codehaus.jackson.map.ObjectMapper import org.codehaus.jackson.map.ObjectMapper
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.InputStream import java.io.InputStream
import java.security.cert.Certificate
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream import java.util.zip.ZipOutputStream
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
@ -26,7 +25,7 @@ import javax.ws.rs.core.Response.Status.UNAUTHORIZED
* Provides functionality for asynchronous submission of certificate signing requests and retrieval of the results. * Provides functionality for asynchronous submission of certificate signing requests and retrieval of the results.
*/ */
@Path("") @Path("")
class DoormanWebService(val intermediateCACertAndKey: CertificateAndKeyPair, val rootCert: Certificate, val storage: CertificationRequestStorage, val serverStatus: DoormanServerStatus) { class DoormanWebService(val csrHandler: DefaultCsrHandler, val serverStatus: DoormanServerStatus) {
@Context lateinit var request: HttpServletRequest @Context lateinit var request: HttpServletRequest
/** /**
* Accept stream of [PKCS10CertificationRequest] from user and persists in [CertificationRequestStorage] for approval. * Accept stream of [PKCS10CertificationRequest] from user and persists in [CertificationRequestStorage] for approval.
@ -43,7 +42,7 @@ class DoormanWebService(val intermediateCACertAndKey: CertificateAndKeyPair, val
// TODO: Certificate signing request verifications. // TODO: Certificate signing request verifications.
// TODO: Use jira api / slack bot to semi automate the approval process? // TODO: Use jira api / slack bot to semi automate the approval process?
// TODO: Acknowledge to user we have received the request via email? // TODO: Acknowledge to user we have received the request via email?
val requestId = storage.saveRequest(CertificationRequestData(request.remoteHost, request.remoteAddr, certificationRequest)) val requestId = csrHandler.saveRequest(CertificationRequestData(request.remoteHost, request.remoteAddr, certificationRequest))
return ok(requestId).build() return ok(requestId).build()
} }
@ -55,22 +54,17 @@ class DoormanWebService(val intermediateCACertAndKey: CertificateAndKeyPair, val
@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 response = storage.getResponse(requestId) val response = csrHandler.getResponse(requestId)
return when (response) { return when (response) {
is CertificateResponse.Ready -> { is CertificateResponse.Ready -> {
// 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.
val baos = ByteArrayOutputStream() val baos = ByteArrayOutputStream()
ZipOutputStream(baos).use { zip -> ZipOutputStream(baos).use { zip ->
// Client certificate must come first and root certificate should come last. // Client certificate must come first and root certificate should come last.
val entries = listOf( val certificates = ArrayList(response.certificatePath.certificates)
CORDA_CLIENT_CA to response.certificate, listOf(CORDA_CLIENT_CA, CORDA_INTERMEDIATE_CA, CORDA_ROOT_CA).zip(certificates).forEach {
CORDA_INTERMEDIATE_CA to intermediateCACertAndKey.certificate.toX509Certificate(),
CORDA_ROOT_CA to rootCert
)
entries.forEach {
zip.putNextEntry(ZipEntry("${it.first}.cer")) zip.putNextEntry(ZipEntry("${it.first}.cer"))
zip.write(it.second.encoded) zip.write(it.second.encoded)
zip.setComment(it.first)
zip.closeEntry() zip.closeEntry()
} }
} }

View File

@ -3,12 +3,12 @@ package com.r3.corda.doorman
import com.atlassian.jira.rest.client.internal.async.AsynchronousJiraRestClientFactory import com.atlassian.jira.rest.client.internal.async.AsynchronousJiraRestClientFactory
import com.google.common.net.HostAndPort import com.google.common.net.HostAndPort
import com.r3.corda.doorman.DoormanServer.Companion.logger import com.r3.corda.doorman.DoormanServer.Companion.logger
import com.r3.corda.doorman.persistence.ApprovingAllCertificateRequestStorage
import com.r3.corda.doorman.persistence.CertificationRequestStorage import com.r3.corda.doorman.persistence.CertificationRequestStorage
import com.r3.corda.doorman.persistence.DBCertificateRequestStorage import com.r3.corda.doorman.persistence.DBCertificateRequestStorage
import com.r3.corda.doorman.persistence.DoormanSchemaService import com.r3.corda.doorman.persistence.DoormanSchemaService
import com.r3.corda.doorman.persistence.JiraCertificateRequestStorage import com.r3.corda.doorman.signer.*
import net.corda.core.crypto.Crypto import net.corda.core.crypto.Crypto
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.createDirectories import net.corda.core.internal.createDirectories
import net.corda.core.utilities.loggerFor import net.corda.core.utilities.loggerFor
import net.corda.core.utilities.seconds import net.corda.core.utilities.seconds
@ -17,10 +17,6 @@ import net.corda.node.utilities.X509Utilities.CORDA_INTERMEDIATE_CA
import net.corda.node.utilities.X509Utilities.CORDA_ROOT_CA import net.corda.node.utilities.X509Utilities.CORDA_ROOT_CA
import net.corda.node.utilities.X509Utilities.createCertificate import net.corda.node.utilities.X509Utilities.createCertificate
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x509.GeneralName
import org.bouncycastle.asn1.x509.GeneralSubtree
import org.bouncycastle.asn1.x509.NameConstraints
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
@ -32,7 +28,6 @@ import java.io.Closeable
import java.lang.Thread.sleep import java.lang.Thread.sleep
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.net.URI import java.net.URI
import java.security.cert.Certificate
import java.time.Instant import java.time.Instant
import kotlin.concurrent.thread import kotlin.concurrent.thread
import kotlin.system.exitProcess import kotlin.system.exitProcess
@ -43,24 +38,25 @@ import kotlin.system.exitProcess
* The server will require keystorePath, keystore password and key password via command line input. * 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] * The Intermediate CA certificate,Intermediate CA private key and Root CA Certificate should use alias name specified in [X509Utilities]
*/ */
class DoormanServer(webServerAddr: HostAndPort, val caCertAndKey: CertificateAndKeyPair, val rootCACert: Certificate, val storage: CertificationRequestStorage) : Closeable { class DoormanServer(webServerAddr: HostAndPort, val csrHandler: DefaultCsrHandler) : Closeable {
val serverStatus = DoormanServerStatus() val serverStatus = DoormanServerStatus()
companion object { companion object {
val logger = loggerFor<DoormanServer>() val logger = loggerFor<DoormanServer>()
} }
private val server: Server = Server(InetSocketAddress(webServerAddr.hostText, webServerAddr.port)).apply { private val server: Server = Server(InetSocketAddress(webServerAddr.host, webServerAddr.port)).apply {
handler = HandlerCollection().apply { handler = HandlerCollection().apply {
addHandler(buildServletContextHandler()) addHandler(buildServletContextHandler())
} }
} }
val hostAndPort: HostAndPort get() = server.connectors val hostAndPort: HostAndPort
.map { it as? ServerConnector } get() = server.connectors
.filterNotNull() .map { it as? ServerConnector }
.map { HostAndPort.fromParts(it.host, it.localPort) } .filterNotNull()
.first() .map { HostAndPort.fromParts(it.host, it.localPort) }
.first()
override fun close() { override fun close() {
logger.info("Shutting down Doorman Web Services...") logger.info("Shutting down Doorman Web Services...")
@ -81,25 +77,7 @@ class DoormanServer(webServerAddr: HostAndPort, val caCertAndKey: CertificateAnd
sleep(10.seconds.toMillis()) sleep(10.seconds.toMillis())
// TODO: Handle rejected request? // TODO: Handle rejected request?
serverStatus.lastRequestCheckTime = Instant.now() serverStatus.lastRequestCheckTime = Instant.now()
for (id in storage.getApprovedRequestIds()) { csrHandler.sign()
storage.approveRequest(id) {
val request = JcaPKCS10CertificationRequest(request)
// The sub certs issued by the client must satisfy this directory name (or legal name in Corda) constraints, sub certs' directory name must be within client CA's name's subtree,
// please see [sun.security.x509.X500Name.isWithinSubtree()] for more information.
// We assume all attributes in the subject name has been checked prior approval.
// TODO: add validation to subject name.
val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, CordaX500Name.build(request.subject).copy(commonName = null).x500Name))), arrayOf())
createCertificate(CertificateType.CLIENT_CA,
caCertAndKey.certificate,
caCertAndKey.keyPair,
CordaX500Name.build(request.subject).copy(commonName = X509Utilities.CORDA_CLIENT_CA_CN),
request.publicKey,
nameConstraints = nameConstraints).toX509Certificate()
}
logger.info("Approved request $id")
serverStatus.lastApprovalTime = Instant.now()
serverStatus.approvedRequests++
}
} catch (e: Exception) { } catch (e: Exception) {
// Log the error and carry on. // Log the error and carry on.
logger.error("Error encountered when approving request.", e) logger.error("Error encountered when approving request.", e)
@ -113,7 +91,7 @@ class DoormanServer(webServerAddr: HostAndPort, val caCertAndKey: CertificateAnd
contextPath = "/" contextPath = "/"
val resourceConfig = ResourceConfig().apply { val resourceConfig = ResourceConfig().apply {
// Add your API provider classes (annotated for JAX-RS) here // Add your API provider classes (annotated for JAX-RS) here
register(DoormanWebService(caCertAndKey, rootCACert, storage, serverStatus)) register(DoormanWebService(csrHandler, serverStatus))
} }
val jerseyServlet = ServletHolder(ServletContainer(resourceConfig)).apply { val jerseyServlet = ServletHolder(ServletContainer(resourceConfig)).apply {
initOrder = 0 // Initialise at server start initOrder = 0 // Initialise at server start
@ -124,12 +102,10 @@ class DoormanServer(webServerAddr: HostAndPort, val caCertAndKey: CertificateAnd
} }
data class DoormanServerStatus(var serverStartTime: Instant? = null, data class DoormanServerStatus(var serverStartTime: Instant? = null,
var lastRequestCheckTime: Instant? = null, var lastRequestCheckTime: Instant? = null)
var lastApprovalTime: Instant? = null,
var approvedRequests: Int = 0)
/** Read password from console, do a readLine instead if console is null (e.g. when debugging in IDE). */ /** Read password from console, do a readLine instead if console is null (e.g. when debugging in IDE). */
private fun readPassword(fmt: String): String { internal fun readPassword(fmt: String): String {
return if (System.console() != null) { return if (System.console() != null) {
String(System.console().readPassword(fmt)) String(System.console().readPassword(fmt))
} else { } else {
@ -139,6 +115,9 @@ private fun readPassword(fmt: String): String {
} }
private fun DoormanParameters.generateRootKeyPair() { private fun DoormanParameters.generateRootKeyPair() {
if (rootStorePath == null) {
throw IllegalArgumentException("The 'rootStorePath' parameter must be specified when generating keys!")
}
println("Generating Root CA keypair and certificate.") println("Generating Root CA keypair and certificate.")
// Get password from console if not in config. // Get password from console if not in config.
val rootKeystorePassword = rootKeystorePassword ?: readPassword("Root Keystore Password: ") val rootKeystorePassword = rootKeystorePassword ?: readPassword("Root Keystore Password: ")
@ -164,6 +143,13 @@ private fun DoormanParameters.generateRootKeyPair() {
} }
private fun DoormanParameters.generateCAKeyPair() { private fun DoormanParameters.generateCAKeyPair() {
if (keystorePath == null) {
throw IllegalArgumentException("The 'keystorePath' parameter must be specified when generating keys!")
}
if (rootStorePath == null) {
throw IllegalArgumentException("The 'rootStorePath' parameter must be specified when generating keys!")
}
println("Generating Intermediate CA keypair and certificate using root keystore $rootStorePath.") println("Generating Intermediate CA keypair and certificate using root keystore $rootStorePath.")
// Get password from console if not in config. // Get password from console if not in config.
val rootKeystorePassword = rootKeystorePassword ?: readPassword("Root Keystore Password: ") val rootKeystorePassword = rootKeystorePassword ?: readPassword("Root Keystore Password: ")
@ -194,40 +180,43 @@ private fun DoormanParameters.generateCAKeyPair() {
println(loadKeyStore(keystorePath, keystorePassword).getCertificate(CORDA_INTERMEDIATE_CA).publicKey) println(loadKeyStore(keystorePath, keystorePassword).getCertificate(CORDA_INTERMEDIATE_CA).publicKey)
} }
private fun DoormanParameters.startDoorman() { private fun DoormanParameters.startDoorman(isLocalSigning: Boolean = false) {
logger.info("Starting Doorman server.") logger.info("Starting Doorman server.")
// Get password from console if not in config.
val keystorePassword = keystorePassword ?: readPassword("Keystore Password: ")
val caPrivateKeyPassword = caPrivateKeyPassword ?: readPassword("CA Private Key Password: ")
val keystore = loadOrCreateKeyStore(keystorePath, keystorePassword)
val rootCACert = keystore.getCertificateChain(CORDA_INTERMEDIATE_CA).last()
val caCertAndKey = keystore.getCertificateAndKeyPair(CORDA_INTERMEDIATE_CA, caPrivateKeyPassword)
// Create DB connection. // Create DB connection.
val database = configureDatabase(dataSourceProperties, databaseProperties, { DoormanSchemaService() }, createIdentityService = { val database = configureDatabase(dataSourceProperties, databaseProperties, { DoormanSchemaService() }, createIdentityService = {
// Identity service not needed doorman, corda persistence is not very generic. // Identity service not needed doorman, corda persistence is not very generic.
throw UnsupportedOperationException() throw UnsupportedOperationException()
}) })
val csrHandler = if (jiraConfig == null) {
val requestStorage = DBCertificateRequestStorage(database)
val storage = if (jiraConfig == null) {
logger.warn("Doorman server is in 'Approve All' mode, this will approve all incoming certificate signing request.") logger.warn("Doorman server is in 'Approve All' mode, this will approve all incoming certificate signing request.")
// Approve all pending request. val storage = ApprovingAllCertificateRequestStorage(database)
object : CertificationRequestStorage by requestStorage { DefaultCsrHandler(storage, buildLocalSigner(storage, this))
// The doorman is in approve all mode, returns all pending request id as approved request id.
override fun getApprovedRequestIds() = getPendingRequestIds()
}
} else { } else {
val storage = DBCertificateRequestStorage(database)
val signer = if (isLocalSigning) {
buildLocalSigner(storage, this)
} else {
ExternalSigner()
}
val jiraClient = AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(URI(jiraConfig.address), jiraConfig.username, jiraConfig.password) val jiraClient = AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(URI(jiraConfig.address), jiraConfig.username, jiraConfig.password)
JiraCertificateRequestStorage(requestStorage, jiraClient, jiraConfig.projectCode, jiraConfig.doneTransitionCode) JiraCsrHandler(jiraClient, jiraConfig.projectCode, jiraConfig.doneTransitionCode, storage, signer)
} }
val doorman = DoormanServer(HostAndPort.fromParts(host, port), csrHandler)
val doorman = DoormanServer(HostAndPort.fromParts(host, port), caCertAndKey, rootCACert, storage)
doorman.start() doorman.start()
Runtime.getRuntime().addShutdownHook(thread(start = false) { doorman.close() }) Runtime.getRuntime().addShutdownHook(thread(start = false) { doorman.close() })
} }
private fun buildLocalSigner(storage: CertificationRequestStorage, parameters: DoormanParameters): Signer {
checkNotNull(parameters.keystorePath) {"The keystorePath parameter must be specified when using local signing!"}
// Get password from console if not in config.
val keystorePassword = parameters.keystorePassword ?: readPassword("Keystore Password: ")
val caPrivateKeyPassword = parameters.caPrivateKeyPassword ?: readPassword("CA Private Key Password: ")
val keystore = loadOrCreateKeyStore(parameters.keystorePath!!, keystorePassword)
val rootCACert = keystore.getCertificateChain(X509Utilities.CORDA_INTERMEDIATE_CA).last()
val caCertAndKey = keystore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, caPrivateKeyPassword)
return LocalSigner(storage, caCertAndKey, rootCACert)
}
fun main(args: Array<String>) { fun main(args: Array<String>) {
try { try {
// TODO : Remove config overrides and solely use config file after testnet is finalized. // TODO : Remove config overrides and solely use config file after testnet is finalized.
@ -235,7 +224,7 @@ fun main(args: Array<String>) {
when (mode) { when (mode) {
DoormanParameters.Mode.ROOT_KEYGEN -> generateRootKeyPair() DoormanParameters.Mode.ROOT_KEYGEN -> generateRootKeyPair()
DoormanParameters.Mode.CA_KEYGEN -> generateCAKeyPair() DoormanParameters.Mode.CA_KEYGEN -> generateCAKeyPair()
DoormanParameters.Mode.DOORMAN -> startDoorman() DoormanParameters.Mode.DOORMAN -> startDoorman(keystorePath != null)
} }
} }
} catch (e: ShowHelpException) { } catch (e: ShowHelpException) {

View File

@ -6,6 +6,7 @@ import joptsimple.ArgumentAcceptingOptionSpec
import joptsimple.OptionParser import joptsimple.OptionParser
import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.cert.X509CertificateHolder
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.security.cert.CertPath
import java.security.cert.Certificate import java.security.cert.Certificate
import java.security.cert.CertificateFactory import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
@ -26,7 +27,7 @@ object OptionParserHelper {
// Convert all command line options to Config. // Convert all command line options to Config.
return ConfigFactory.parseMap(parser.recognizedOptions().mapValues { return ConfigFactory.parseMap(parser.recognizedOptions().mapValues {
val optionSpec = it.value val optionSpec = it.value
if (optionSpec is ArgumentAcceptingOptionSpec<*> && !optionSpec.requiresArgument() && optionSet.has(optionSpec)) true else optionSpec.value(optionSet) if (optionSpec is ArgumentAcceptingOptionSpec<*> && !optionSpec.requiresArgument() && optionSet.has(optionSpec)) null else optionSpec.value(optionSet)
}.filterValues { it != null }) }.filterValues { it != null })
} }
} }
@ -40,3 +41,7 @@ object CertificateUtilities {
} }
fun X509CertificateHolder.toX509Certificate(): Certificate = CertificateUtilities.toX509Certificate(encoded) fun X509CertificateHolder.toX509Certificate(): Certificate = CertificateUtilities.toX509Certificate(encoded)
fun buildCertPath(vararg certificates: Certificate): CertPath {
return CertificateFactory.getInstance("X509").generateCertPath(certificates.asList())
}

View File

@ -0,0 +1,14 @@
package com.r3.corda.doorman.persistence
import net.corda.node.utilities.CordaPersistence
/**
* This storage automatically approves all created requests.
*/
class ApprovingAllCertificateRequestStorage(private val database: CordaPersistence) : DBCertificateRequestStorage(database) {
override fun saveRequest(certificationData: CertificationRequestData): String {
val requestId = saveRequest(certificationData)
approveRequest(requestId)
return requestId
}
}

View File

@ -1,14 +1,19 @@
package com.r3.corda.doorman.persistence package com.r3.corda.doorman.persistence
import org.bouncycastle.pkcs.PKCS10CertificationRequest import org.bouncycastle.pkcs.PKCS10CertificationRequest
import java.security.cert.Certificate import java.security.cert.CertPath
/** /**
* Provide certificate signing request storage for the certificate signing server. * Provide certificate signing request storage for the certificate signing server.
*/ */
interface CertificationRequestStorage { interface CertificationRequestStorage {
companion object {
val DOORMAN_SIGNATURE = listOf("Doorman")
}
/** /**
* Persist [certificationData] in storage for further approval if it's a valid request. If not then it will be automically * Persist [certificationData] in storage for further approval if it's a valid request. If not then it will be automatically
* rejected and not subject to any approval process. In both cases a randomly generated request ID is returned. * rejected and not subject to any approval process. In both cases a randomly generated request ID is returned.
*/ */
fun saveRequest(certificationData: CertificationRequestData): String fun saveRequest(certificationData: CertificationRequestData): String
@ -24,30 +29,44 @@ interface CertificationRequestStorage {
fun getResponse(requestId: String): CertificateResponse fun getResponse(requestId: String): CertificateResponse
/** /**
* Approve the given request by generating and storing a new certificate using the provided generator. * Approve the given request if it has not already been approved. Otherwise do nothing.
*
* @return True if the request has been approved and false otherwise.
*/ */
fun approveRequest(requestId: String, generateCertificate: CertificationRequestData.() -> Certificate) fun approveRequest(requestId: String, approvedBy: String = DOORMAN_SIGNATURE.first()): Boolean
/**
* Signs the certificate signing request by assigning the given certificate.
*
* @return True if the request has been signed and false otherwise.
*/
fun signCertificate(requestId: String, signedBy: List<String> = DOORMAN_SIGNATURE, generateCertificate: CertificationRequestData.() -> CertPath): Boolean
/** /**
* Reject the given request using the given reason. * Reject the given request using the given reason.
*/ */
fun rejectRequest(requestId: String, rejectReason: String) fun rejectRequest(requestId: String, rejectedBy: String = DOORMAN_SIGNATURE.first(), rejectReason: String)
/** /**
* Retrieve list of request IDs waiting for approval. * Retrieve list of request IDs waiting for approval.
*/ */
fun getPendingRequestIds(): List<String> fun getNewRequestIds(): List<String>
/** /**
* Retrieve list of approved request IDs. * Retrieve list of approved request IDs.
*/ */
fun getApprovedRequestIds(): List<String> fun getApprovedRequestIds(): List<String>
/**
* Retrieve list of signed request IDs.
*/
fun getSignedRequestIds(): List<String>
} }
data class CertificationRequestData(val hostName: String, val ipAddress: String, val request: PKCS10CertificationRequest) data class CertificationRequestData(val hostName: String, val ipAddress: String, val request: PKCS10CertificationRequest)
sealed class CertificateResponse { sealed class CertificateResponse {
object NotReady : CertificateResponse() object NotReady : CertificateResponse()
class Ready(val certificate: Certificate) : CertificateResponse() class Ready(val certificatePath: CertPath) : CertificateResponse()
class Unauthorised(val message: String) : CertificateResponse() class Unauthorised(val message: String) : CertificateResponse()
} }

View File

@ -1,11 +1,12 @@
package com.r3.corda.doorman.persistence package com.r3.corda.doorman.persistence
import com.r3.corda.doorman.CertificateUtilities
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.identity.CordaX500Name import net.corda.core.identity.CordaX500Name
import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.CordaPersistence
import org.bouncycastle.pkcs.PKCS10CertificationRequest import org.bouncycastle.pkcs.PKCS10CertificationRequest
import java.security.cert.Certificate import java.io.ByteArrayInputStream
import java.security.cert.CertPath
import java.security.cert.CertificateFactory
import java.time.Instant import java.time.Instant
import javax.persistence.* import javax.persistence.*
import javax.persistence.criteria.CriteriaBuilder import javax.persistence.criteria.CriteriaBuilder
@ -13,7 +14,7 @@ import javax.persistence.criteria.Path
import javax.persistence.criteria.Predicate import javax.persistence.criteria.Predicate
// TODO Relax the uniqueness requirement to be on the entire X.500 subject rather than just the legal name // TODO Relax the uniqueness requirement to be on the entire X.500 subject rather than just the legal name
class DBCertificateRequestStorage(private val database: CordaPersistence) : CertificationRequestStorage { open class DBCertificateRequestStorage(private val database: CordaPersistence) : CertificationRequestStorage {
@Entity @Entity
@Table(name = "certificate_signing_request") @Table(name = "certificate_signing_request")
class CertificateSigningRequest( class CertificateSigningRequest(
@ -34,20 +35,43 @@ class DBCertificateRequestStorage(private val database: CordaPersistence) : Cert
@Column @Column
var request: ByteArray = ByteArray(0), var request: ByteArray = ByteArray(0),
@Column(name = "request_timestamp") @Column(name = "created_at")
var requestTimestamp: Instant = Instant.now(), var createdAt: Instant = Instant.now(),
@Column(name = "process_timestamp", nullable = true) @Column(name = "approved_at")
var processTimestamp: Instant? = null, var approvedAt: Instant = Instant.now(),
@Column(name = "approved_by", length = 64)
var approvedBy: String? = null,
@Column
var status: Status = Status.New,
@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 = "rejected_by", length = 64)
var rejectedBy: String? = null,
@Column(name = "rejected_at")
var rejectedAt: Instant? = Instant.now(),
@Lob @Lob
@Column(nullable = true) @Column(nullable = true)
var certificate: ByteArray? = null, var certificatePath: ByteArray? = null,
@Column(name = "reject_reason", length = 256, nullable = true) @Column(name = "reject_reason", length = 256, nullable = true)
var rejectReason: String? = null var rejectReason: String? = null
) )
enum class Status {
New, Approved, Rejected, Signed
}
override fun saveRequest(certificationData: CertificationRequestData): String { override fun saveRequest(certificationData: CertificationRequestData): String {
val requestId = SecureHash.randomSHA256().toString() val requestId = SecureHash.randomSHA256().toString()
@ -60,9 +84,8 @@ class DBCertificateRequestStorage(private val database: CordaPersistence) : Cert
val criteriaQuery = createQuery(CertificateSigningRequest::class.java) val criteriaQuery = createQuery(CertificateSigningRequest::class.java)
criteriaQuery.from(CertificateSigningRequest::class.java).run { criteriaQuery.from(CertificateSigningRequest::class.java).run {
val nameEq = equal(get<String>(CertificateSigningRequest::legalName.name), legalName.toString()) val nameEq = equal(get<String>(CertificateSigningRequest::legalName.name), legalName.toString())
val certNotNull = isNotNull(get<String>(CertificateSigningRequest::certificate.name)) val statusNewOrApproved = get<String>(CertificateSigningRequest::status.name).`in`(Status.Approved, Status.New)
val processTimeIsNull = isNull(get<String>(CertificateSigningRequest::processTimestamp.name)) criteriaQuery.where(and(nameEq, statusNewOrApproved))
criteriaQuery.where(and(nameEq, or(certNotNull, processTimeIsNull)))
} }
} }
val duplicate = session.createQuery(query).resultList.isNotEmpty() val duplicate = session.createQuery(query).resultList.isNotEmpty()
@ -74,16 +97,14 @@ class DBCertificateRequestStorage(private val database: CordaPersistence) : Cert
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
Pair(certificationData.request.subject, "Name validation failed with exception : ${e.message}") Pair(certificationData.request.subject, "Name validation failed with exception : ${e.message}")
} }
val now = Instant.now()
val request = CertificateSigningRequest( val request = CertificateSigningRequest(
requestId, requestId = requestId,
certificationData.hostName, hostName = certificationData.hostName,
certificationData.ipAddress, ipAddress = certificationData.ipAddress,
legalName.toString(), legalName = legalName.toString(),
certificationData.request.encoded, request = certificationData.request.encoded,
now,
rejectReason = rejectReason, rejectReason = rejectReason,
processTimestamp = rejectReason?.let { now } status = if (rejectReason == null) Status.New else Status.Rejected
) )
session.save(request) session.save(request)
} }
@ -93,49 +114,68 @@ class DBCertificateRequestStorage(private val database: CordaPersistence) : Cert
override fun getResponse(requestId: String): CertificateResponse { override fun getResponse(requestId: String): CertificateResponse {
return database.transaction { return database.transaction {
val response = singleRequestWhere { builder, path -> val response = singleRequestWhere { builder, path ->
val eq = builder.equal(path.get<String>(CertificateSigningRequest::requestId.name), requestId) builder.equal(path.get<String>(CertificateSigningRequest::requestId.name), requestId)
val timeNotNull = builder.isNotNull(path.get<Instant>(CertificateSigningRequest::processTimestamp.name))
builder.and(eq, timeNotNull)
} }
if (response == null) { if (response == null) {
CertificateResponse.NotReady CertificateResponse.NotReady
} else { } else {
val certificate = response.certificate when (response.status) {
if (certificate != null) { Status.New, Status.Approved -> CertificateResponse.NotReady
CertificateResponse.Ready(CertificateUtilities.toX509Certificate(certificate)) Status.Rejected -> CertificateResponse.Unauthorised(response.rejectReason ?: "Unknown reason")
} else { Status.Signed -> CertificateResponse.Ready(buildCertPath(response.certificatePath))
CertificateResponse.Unauthorised(response.rejectReason!!)
} }
} }
} }
} }
override fun approveRequest(requestId: String, generateCertificate: CertificationRequestData.() -> Certificate) { override fun approveRequest(requestId: String, approvedBy: String): Boolean {
var approved = false
database.transaction { database.transaction {
val request = singleRequestWhere { builder, path -> val request = singleRequestWhere { builder, path ->
val eq = builder.equal(path.get<String>(CertificateSigningRequest::requestId.name), requestId) builder.and(builder.equal(path.get<String>(CertificateSigningRequest::requestId.name), requestId),
val timeIsNull = builder.isNull(path.get<Instant>(CertificateSigningRequest::processTimestamp.name)) builder.equal(path.get<String>(CertificateSigningRequest::status.name), Status.New))
builder.and(eq, timeIsNull)
} }
if (request != null) { if (request != null) {
request.certificate = request.toRequestData().generateCertificate().encoded request.approvedAt = Instant.now()
request.processTimestamp = Instant.now() request.approvedBy = approvedBy
request.status = Status.Approved
session.save(request) session.save(request)
approved = true
} }
} }
return approved
} }
override fun rejectRequest(requestId: String, rejectReason: String) { override fun signCertificate(requestId: String, signedBy: List<String>, generateCertificate: CertificationRequestData.() -> CertPath): Boolean {
var signed = false
database.transaction { database.transaction {
val request = singleRequestWhere { builder, path -> val request = singleRequestWhere { builder, path ->
val eq = builder.equal(path.get<String>(CertificateSigningRequest::requestId.name), requestId) builder.and(builder.equal(path.get<String>(CertificateSigningRequest::requestId.name), requestId),
val timeIsNull = builder.isNull(path.get<Instant>(CertificateSigningRequest::processTimestamp.name)) builder.equal(path.get<String>(CertificateSigningRequest::status.name), Status.Approved))
builder.and(eq, timeIsNull) }
if (request != null) {
val now = Instant.now()
request.certificatePath = request.toRequestData().generateCertificate().encoded
request.status = Status.Signed
request.signedAt = now
request.signedBy = signedBy
session.save(request)
signed = true
}
}
return signed
}
override fun rejectRequest(requestId: String, rejectedBy: String, rejectReason: String) {
database.transaction {
val request = singleRequestWhere { builder, path ->
builder.equal(path.get<String>(CertificateSigningRequest::requestId.name), requestId)
} }
if (request != null) { if (request != null) {
request.rejectReason = rejectReason request.rejectReason = rejectReason
request.processTimestamp = Instant.now() request.status = Status.Rejected
request.rejectedBy = rejectedBy
request.rejectedAt = Instant.now()
session.save(request) session.save(request)
} }
} }
@ -149,21 +189,31 @@ class DBCertificateRequestStorage(private val database: CordaPersistence) : Cert
}?.toRequestData() }?.toRequestData()
} }
override fun getPendingRequestIds(): List<String> { override fun getApprovedRequestIds(): List<String> {
return getRequestIdsByStatus(Status.Approved)
}
override fun getNewRequestIds(): List<String> {
return getRequestIdsByStatus(Status.New)
}
override fun getSignedRequestIds(): List<String> {
return getRequestIdsByStatus(Status.Signed)
}
private fun getRequestIdsByStatus(status: Status): List<String> {
return database.transaction { return database.transaction {
val builder = session.criteriaBuilder val builder = session.criteriaBuilder
val query = builder.createQuery(String::class.java).run { val query = builder.createQuery(String::class.java).run {
from(CertificateSigningRequest::class.java).run { from(CertificateSigningRequest::class.java).run {
select(get(CertificateSigningRequest::requestId.name)) select(get(CertificateSigningRequest::requestId.name))
where(builder.isNull(get<String>(CertificateSigningRequest::processTimestamp.name))) where(builder.equal(get<Status>(CertificateSigningRequest::status.name), status))
} }
} }
session.createQuery(query).resultList session.createQuery(query).resultList
} }
} }
override fun getApprovedRequestIds(): List<String> = emptyList()
private fun singleRequestWhere(predicate: (CriteriaBuilder, Path<CertificateSigningRequest>) -> Predicate): CertificateSigningRequest? { private fun singleRequestWhere(predicate: (CriteriaBuilder, Path<CertificateSigningRequest>) -> Predicate): CertificateSigningRequest? {
return database.transaction { return database.transaction {
val builder = session.criteriaBuilder val builder = session.criteriaBuilder
@ -176,4 +226,6 @@ class DBCertificateRequestStorage(private val database: CordaPersistence) : Cert
} }
private fun CertificateSigningRequest.toRequestData() = CertificationRequestData(hostName, ipAddress, PKCS10CertificationRequest(request)) private fun CertificateSigningRequest.toRequestData() = CertificationRequestData(hostName, ipAddress, PKCS10CertificationRequest(request))
private fun buildCertPath(certPathBytes: ByteArray?) = CertificateFactory.getInstance("X509").generateCertPath(ByteArrayInputStream(certPathBytes))
} }

View File

@ -1,80 +0,0 @@
package com.r3.corda.doorman.persistence
import com.atlassian.jira.rest.client.api.JiraRestClient
import com.atlassian.jira.rest.client.api.domain.Field
import com.atlassian.jira.rest.client.api.domain.IssueType
import com.atlassian.jira.rest.client.api.domain.input.IssueInputBuilder
import com.atlassian.jira.rest.client.api.domain.input.TransitionInput
import net.corda.core.utilities.country
import net.corda.core.utilities.locality
import net.corda.core.utilities.loggerFor
import net.corda.core.utilities.organisation
import net.corda.node.utilities.X509Utilities
import org.bouncycastle.asn1.x500.style.BCStyle
import org.bouncycastle.openssl.jcajce.JcaPEMWriter
import org.bouncycastle.util.io.pem.PemObject
import java.io.StringWriter
import java.security.cert.Certificate
class JiraCertificateRequestStorage(val delegate: CertificationRequestStorage,
val jiraClient: JiraRestClient,
val projectCode: String,
val doneTransitionCode: Int) : CertificationRequestStorage by delegate {
private enum class Status {
Approved, Rejected
}
companion object {
private val logger = loggerFor<JiraCertificateRequestStorage>()
}
// The JIRA project must have a Request ID field and the Task issue type.
private val requestIdField: Field = jiraClient.metadataClient.fields.claim().find { it.name == "Request ID" }!!
private val taskIssueType: IssueType = jiraClient.metadataClient.issueTypes.claim().find { it.name == "Task" }!!
override fun saveRequest(certificationData: CertificationRequestData): String {
val requestId = delegate.saveRequest(certificationData)
// Make sure request has been accepted.
val response = getResponse(requestId)
if (response !is CertificateResponse.Unauthorised) {
val request = StringWriter()
JcaPEMWriter(request).use {
it.writeObject(PemObject("CERTIFICATE REQUEST", certificationData.request.encoded))
}
val organisation = certificationData.request.subject.organisation
val nearestCity = certificationData.request.subject.locality
val country = certificationData.request.subject.country
val email = certificationData.request.getAttributes(BCStyle.E).firstOrNull()?.attrValues?.firstOrNull()?.toString()
val issue = IssueInputBuilder().setIssueTypeId(taskIssueType.id)
.setProjectKey(projectCode)
.setDescription("Organisation: $organisation\nNearest City: $nearestCity\nCountry: $country\nEmail: $email\n\n{code}$request{code}")
.setSummary(organisation)
.setFieldValue(requestIdField.id, requestId)
// This will block until the issue is created.
jiraClient.issueClient.createIssue(issue.build()).fail { logger.error("Exception when creating JIRA issue.", it) }.claim()
}
return requestId
}
override fun approveRequest(requestId: String, generateCertificate: CertificationRequestData.() -> Certificate) {
delegate.approveRequest(requestId, generateCertificate)
// Certificate should be created, retrieving it to attach to the jira task.
val certificate = (getResponse(requestId) as? CertificateResponse.Ready)?.certificate
// Jira only support ~ (contains) search for custom textfield.
val issue = jiraClient.searchClient.searchJql("'Request ID' ~ $requestId").claim().issues.firstOrNull()
if (issue != null) {
jiraClient.issueClient.transition(issue, TransitionInput(doneTransitionCode)).fail { logger.error("Exception when transiting JIRA status.", it) }.claim()
jiraClient.issueClient.addAttachment(issue.attachmentsUri, certificate?.encoded?.inputStream(), "${X509Utilities.CORDA_CLIENT_CA}.cer")
.fail { logger.error("Exception when uploading attachment to JIRA.", it) }.claim()
}
}
override fun getApprovedRequestIds(): List<String> = getRequestByStatus(Status.Approved)
private fun getRequestByStatus(status: Status): List<String> {
val issues = jiraClient.searchClient.searchJql("project = $projectCode AND status = $status").claim().issues
return issues.map { it.getField(requestIdField.id)?.value?.toString() }.filterNotNull()
}
}

View File

@ -0,0 +1,100 @@
package com.r3.corda.doorman.signer
import com.atlassian.jira.rest.client.api.JiraRestClient
import com.atlassian.jira.rest.client.api.domain.Field
import com.atlassian.jira.rest.client.api.domain.IssueType
import com.atlassian.jira.rest.client.api.domain.input.IssueInputBuilder
import com.atlassian.jira.rest.client.api.domain.input.TransitionInput
import com.r3.corda.doorman.persistence.CertificateResponse
import com.r3.corda.doorman.persistence.CertificationRequestData
import com.r3.corda.doorman.persistence.CertificationRequestStorage
import net.corda.core.utilities.country
import net.corda.core.utilities.locality
import net.corda.core.utilities.loggerFor
import net.corda.core.utilities.organisation
import net.corda.node.utilities.X509Utilities
import org.bouncycastle.asn1.x500.style.BCStyle
import org.bouncycastle.openssl.jcajce.JcaPEMWriter
import org.bouncycastle.util.io.pem.PemObject
import java.io.StringWriter
open class DefaultCsrHandler(protected val storage: CertificationRequestStorage, protected val signer: Signer) {
open fun saveRequest(certificationData: CertificationRequestData): String {
return storage.saveRequest(certificationData)
}
open fun sign() {
for (id in storage.getApprovedRequestIds()) {
signer.sign(id)
}
}
fun getResponse(requestId: String) = storage.getResponse(requestId)
}
class JiraCsrHandler(private val jiraClient: JiraRestClient,
private val projectCode: String,
private val doneTransitionCode: Int,
storage: CertificationRequestStorage, signer: Signer) : DefaultCsrHandler(storage, signer) {
companion object {
private val logger = loggerFor<JiraCsrHandler>()
}
// The JIRA project must have a Request ID field and the Task issue type.
private val requestIdField: Field = jiraClient.metadataClient.fields.claim().find { it.name == "Request ID" }!!
private val taskIssueType: IssueType = jiraClient.metadataClient.issueTypes.claim().find { it.name == "Task" }!!
override fun saveRequest(certificationData: CertificationRequestData): String {
val requestId = super.saveRequest(certificationData)
// Make sure request has been accepted.
val response = storage.getResponse(requestId)
if (response !is CertificateResponse.Unauthorised) {
val request = StringWriter()
JcaPEMWriter(request).use {
it.writeObject(PemObject("CERTIFICATE REQUEST", certificationData.request.encoded))
}
val organisation = certificationData.request.subject.organisation
val nearestCity = certificationData.request.subject.locality
val country = certificationData.request.subject.country
val email = certificationData.request.getAttributes(BCStyle.E).firstOrNull()?.attrValues?.firstOrNull()?.toString()
val issue = IssueInputBuilder().setIssueTypeId(taskIssueType.id)
.setProjectKey(projectCode)
.setDescription("Organisation: $organisation\nNearest City: $nearestCity\nCountry: $country\nEmail: $email\n\n{code}$request{code}")
.setSummary(organisation)
.setFieldValue(requestIdField.id, requestId)
// This will block until the issue is created.
jiraClient.issueClient.createIssue(issue.build()).fail { logger.error("Exception when creating JIRA issue.", it) }.claim()
}
return requestId
}
override fun sign() {
val issues = jiraClient.searchClient.searchJql("project = $projectCode AND status = Approved").claim().issues
issues.map {
val requestId = it.getField(requestIdField.id)?.value?.toString()
if (requestId != null) {
var approvedBy = it.assignee?.displayName
if (approvedBy == null) {
approvedBy = "Unknown"
}
storage.approveRequest(requestId, approvedBy)
}
}
super.sign()
// Retrieving certificates for signed CSRs to attach to the jira tasks.
storage.getSignedRequestIds().forEach {
val certificate = (storage.getResponse(it) as? CertificateResponse.Ready)?.certificatePath!!.certificates.first()
// Jira only support ~ (contains) search for custom textfield.
val issue = jiraClient.searchClient.searchJql("'Request ID' ~ $it").claim().issues.firstOrNull()
if (issue != null) {
jiraClient.issueClient.transition(issue, TransitionInput(doneTransitionCode)).fail { logger.error("Exception when transiting JIRA status.", it) }.claim()
jiraClient.issueClient.addAttachment(issue.attachmentsUri, certificate?.encoded?.inputStream(), "${X509Utilities.CORDA_CLIENT_CA}.cer")
.fail { logger.error("Exception when uploading attachment to JIRA.", it) }.claim()
}
}
}
}

View File

@ -0,0 +1,48 @@
package com.r3.corda.doorman.signer
import com.r3.corda.doorman.buildCertPath
import com.r3.corda.doorman.persistence.CertificationRequestStorage
import com.r3.corda.doorman.toX509Certificate
import net.corda.core.identity.CordaX500Name
import net.corda.node.utilities.CertificateAndKeyPair
import net.corda.node.utilities.CertificateType
import net.corda.node.utilities.X509Utilities
import org.bouncycastle.asn1.x509.GeneralName
import org.bouncycastle.asn1.x509.GeneralSubtree
import org.bouncycastle.asn1.x509.NameConstraints
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
import java.security.cert.Certificate
interface Signer {
fun sign(requestId: String)
}
class LocalSigner(private val storage: CertificationRequestStorage,
private val caCertAndKey: CertificateAndKeyPair,
private val rootCACert: Certificate) : Signer {
override fun sign(requestId: String) {
storage.signCertificate(requestId) {
val request = JcaPKCS10CertificationRequest(request)
// The sub certs issued by the client must satisfy this directory name (or legal name in Corda) constraints, sub certs' directory name must be within client CA's name's subtree,
// please see [sun.security.x509.X500Name.isWithinSubtree()] for more information.
// We assume all attributes in the subject name has been checked prior approval.
// TODO: add validation to subject name.
val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, CordaX500Name.build(request.subject).copy(commonName = null).x500Name))), arrayOf())
val ourCertificate = caCertAndKey.certificate
val clientCertificate = X509Utilities.createCertificate(CertificateType.CLIENT_CA,
caCertAndKey.certificate,
caCertAndKey.keyPair,
CordaX500Name.build(request.subject).copy(commonName = X509Utilities.CORDA_CLIENT_CA_CN),
request.publicKey,
nameConstraints = nameConstraints).toX509Certificate()
buildCertPath(clientCertificate, ourCertificate.toX509Certificate(), rootCACert)
}
}
}
class ExternalSigner : Signer {
override fun sign(requestId: String) {
// Do nothing
}
}

View File

@ -14,9 +14,9 @@ class DoormanParametersTest {
@Test @Test
fun `parse mode flag arg correctly`() { fun `parse mode flag arg correctly`() {
assertEquals(DoormanParameters.Mode.CA_KEYGEN, parseParameters("--keygen", "--configFile", validConfigPath).mode) assertEquals(DoormanParameters.Mode.CA_KEYGEN, parseParameters("--mode", "CA_KEYGEN", "--configFile", validConfigPath).mode)
assertEquals(DoormanParameters.Mode.ROOT_KEYGEN, parseParameters("--rootKeygen", "--configFile", validConfigPath).mode) assertEquals(DoormanParameters.Mode.ROOT_KEYGEN, parseParameters("--mode", "ROOT_KEYGEN", "--configFile", validConfigPath).mode)
assertEquals(DoormanParameters.Mode.DOORMAN, parseParameters("--configFile", validConfigPath).mode) assertEquals(DoormanParameters.Mode.DOORMAN, parseParameters("--mode", "DOORMAN", "--configFile", validConfigPath).mode)
} }
@Test @Test
@ -34,7 +34,7 @@ class DoormanParametersTest {
fun `should fail when config missing`() { fun `should fail when config missing`() {
// dataSourceProperties is missing from node_fail.conf and it should fail during parsing, and shouldn't use default from reference.conf. // dataSourceProperties is missing from node_fail.conf and it should fail during parsing, and shouldn't use default from reference.conf.
assertFailsWith<ConfigException.Missing> { assertFailsWith<ConfigException.Missing> {
parseParameters("--keygen", "--keystorePath", testDummyPath, "--configFile", invalidConfigPath) parseParameters("--configFile", invalidConfigPath)
} }
} }

View File

@ -5,6 +5,8 @@ import com.nhaarman.mockito_kotlin.*
import com.r3.corda.doorman.persistence.CertificateResponse import com.r3.corda.doorman.persistence.CertificateResponse
import com.r3.corda.doorman.persistence.CertificationRequestData import com.r3.corda.doorman.persistence.CertificationRequestData
import com.r3.corda.doorman.persistence.CertificationRequestStorage import com.r3.corda.doorman.persistence.CertificationRequestStorage
import com.r3.corda.doorman.signer.DefaultCsrHandler
import com.r3.corda.doorman.signer.LocalSigner
import net.corda.core.crypto.Crypto import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.node.utilities.CertificateAndKeyPair import net.corda.node.utilities.CertificateAndKeyPair
@ -27,7 +29,7 @@ import java.io.IOException
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.HttpURLConnection.* import java.net.HttpURLConnection.*
import java.net.URL import java.net.URL
import java.security.cert.Certificate import java.security.cert.CertPath
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import java.util.* import java.util.*
import java.util.zip.ZipInputStream import java.util.zip.ZipInputStream
@ -42,7 +44,9 @@ class DoormanServiceTest {
private lateinit var doormanServer: DoormanServer private lateinit var doormanServer: DoormanServer
private fun startSigningServer(storage: CertificationRequestStorage) { private fun startSigningServer(storage: CertificationRequestStorage) {
doormanServer = DoormanServer(HostAndPort.fromParts("localhost", 0), CertificateAndKeyPair(intermediateCACert, intermediateCAKey), rootCACert.toX509Certificate(), storage) val caCertAndKey = CertificateAndKeyPair(intermediateCACert, intermediateCAKey)
val csrManager = DefaultCsrHandler(storage, LocalSigner(storage, caCertAndKey, rootCACert.toX509Certificate()))
doormanServer = DoormanServer(HostAndPort.fromParts("localhost", 0), csrManager)
doormanServer.start() doormanServer.start()
} }
@ -77,28 +81,32 @@ class DoormanServiceTest {
val id = SecureHash.randomSHA256().toString() val id = SecureHash.randomSHA256().toString()
// Mock Storage behaviour. // Mock Storage behaviour.
val certificateStore = mutableMapOf<String, Certificate>() val certificateStore = mutableMapOf<String, CertPath>()
val storage = mock<CertificationRequestStorage> { val storage = mock<CertificationRequestStorage> {
on { getResponse(eq(id)) }.then { on { getResponse(eq(id)) }.then {
certificateStore[id]?.let { CertificateResponse.Ready(it) } ?: CertificateResponse.NotReady certificateStore[id]?.let {
CertificateResponse.Ready(it)
} ?: CertificateResponse.NotReady
} }
on { approveRequest(eq(id), any()) }.then { on { signCertificate(eq(id), any(), any()) }.then {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
val certGen = it.arguments[1] as ((CertificationRequestData) -> Certificate) val certGen = it.arguments[2] as ((CertificationRequestData) -> CertPath)
val request = CertificationRequestData("", "", X509Utilities.createCertificateSigningRequest(X500Name("CN=LegalName,L=London"), "my@mail.com", keyPair)) val request = CertificationRequestData("", "", X509Utilities.createCertificateSigningRequest(X500Name("CN=LegalName,L=London"), "my@mail.com", keyPair))
certificateStore[id] = certGen(request) certificateStore[id] = certGen(request)
Unit true
} }
on { getPendingRequestIds() }.then { listOf(id) } on { getNewRequestIds() }.then { listOf(id) }
} }
startSigningServer(storage) startSigningServer(storage)
assertThat(pollForResponse(id)).isEqualTo(PollResponse.NotReady) assertThat(pollForResponse(id)).isEqualTo(PollResponse.NotReady)
storage.approveRequest(id) { storage.approveRequest(id)
storage.signCertificate(id) {
JcaPKCS10CertificationRequest(request).run { JcaPKCS10CertificationRequest(request).run {
X509Utilities.createCertificate(CertificateType.TLS, intermediateCACert, intermediateCAKey, subject, publicKey).toX509Certificate() val tlsCert = X509Utilities.createCertificate(CertificateType.TLS, intermediateCACert, intermediateCAKey, subject, publicKey).toX509Certificate()
buildCertPath(tlsCert, intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate())
} }
} }
@ -123,29 +131,33 @@ class DoormanServiceTest {
val id = SecureHash.randomSHA256().toString() val id = SecureHash.randomSHA256().toString()
// Mock Storage behaviour. // Mock Storage behaviour.
val certificateStore = mutableMapOf<String, Certificate>() val certificateStore = mutableMapOf<String, CertPath>()
val storage = mock<CertificationRequestStorage> { val storage = mock<CertificationRequestStorage> {
on { getResponse(eq(id)) }.then { on { getResponse(eq(id)) }.then {
certificateStore[id]?.let { CertificateResponse.Ready(it) } ?: CertificateResponse.NotReady certificateStore[id]?.let {
CertificateResponse.Ready(it)
} ?: CertificateResponse.NotReady
} }
on { approveRequest(eq(id), any()) }.then { on { signCertificate(eq(id), any(), any()) }.then {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
val certGen = it.arguments[1] as ((CertificationRequestData) -> Certificate) val certGen = it.arguments[2] as ((CertificationRequestData) -> CertPath)
val request = CertificationRequestData("", "", X509Utilities.createCertificateSigningRequest(X500Name("CN=LegalName,L=London"), "my@mail.com", keyPair)) val request = CertificationRequestData("", "", X509Utilities.createCertificateSigningRequest(X500Name("CN=LegalName,L=London"), "my@mail.com", keyPair))
certificateStore[id] = certGen(request) certificateStore[id] = certGen(request)
Unit true
} }
on { getPendingRequestIds() }.then { listOf(id) } on { getNewRequestIds() }.then { listOf(id) }
} }
startSigningServer(storage) startSigningServer(storage)
assertThat(pollForResponse(id)).isEqualTo(PollResponse.NotReady) assertThat(pollForResponse(id)).isEqualTo(PollResponse.NotReady)
storage.approveRequest(id) { storage.approveRequest(id)
storage.signCertificate(id) {
JcaPKCS10CertificationRequest(request).run { JcaPKCS10CertificationRequest(request).run {
val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, X500Name("CN=LegalName, L=London")))), arrayOf()) val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, X500Name("CN=LegalName, L=London")))), arrayOf())
X509Utilities.createCertificate(CertificateType.CLIENT_CA, intermediateCACert, intermediateCAKey, subject, publicKey, nameConstraints = nameConstraints).toX509Certificate() val clientCert = X509Utilities.createCertificate(CertificateType.CLIENT_CA, intermediateCACert, intermediateCAKey, subject, publicKey, nameConstraints = nameConstraints).toX509Certificate()
buildCertPath(clientCert, intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate())
} }
} }
@ -166,7 +178,7 @@ class DoormanServiceTest {
val storage = mock<CertificationRequestStorage> { val storage = mock<CertificationRequestStorage> {
on { getResponse(eq(id)) }.then { CertificateResponse.Unauthorised("Not Allowed") } on { getResponse(eq(id)) }.then { CertificateResponse.Unauthorised("Not Allowed") }
on { getPendingRequestIds() }.then { listOf(id) } on { getNewRequestIds() }.then { listOf(id) }
} }
startSigningServer(storage) startSigningServer(storage)

View File

@ -1,5 +1,6 @@
package com.r3.corda.doorman.internal.persistence package com.r3.corda.doorman.internal.persistence
import com.r3.corda.doorman.buildCertPath
import com.r3.corda.doorman.persistence.CertificateResponse import com.r3.corda.doorman.persistence.CertificateResponse
import com.r3.corda.doorman.persistence.CertificationRequestData import com.r3.corda.doorman.persistence.CertificationRequestData
import com.r3.corda.doorman.persistence.DBCertificateRequestStorage import com.r3.corda.doorman.persistence.DBCertificateRequestStorage
@ -13,22 +14,20 @@ import net.corda.node.utilities.CordaPersistence
import net.corda.node.utilities.X509Utilities import net.corda.node.utilities.X509Utilities
import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.configureDatabase
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.bouncycastle.asn1.x509.GeneralName import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x509.GeneralSubtree
import org.bouncycastle.asn1.x509.NameConstraints
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import java.security.KeyPair import java.security.KeyPair
import java.security.cert.CertPath
import java.util.* import java.util.*
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull import kotlin.test.assertNotNull
import kotlin.test.assertTrue import kotlin.test.assertTrue
class DBCertificateRequestStorageTest { 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 storage: DBCertificateRequestStorage
private lateinit var persistence: CordaPersistence private lateinit var persistence: CordaPersistence
@ -52,32 +51,102 @@ class DBCertificateRequestStorageTest {
assertEquals(request.ipAddress, ipAddress) assertEquals(request.ipAddress, ipAddress)
assertEquals(request.request, this.request) assertEquals(request.request, this.request)
} }
assertThat(storage.getPendingRequestIds()).containsOnly(requestId) assertThat(storage.getNewRequestIds()).containsOnly(requestId)
} }
@Test @Test
fun `approve request`() { fun `approve request`() {
val (request, keyPair) = createRequest("LegalName") val (request, _) = createRequest("LegalName")
// Add request to DB. // Add request to DB.
val requestId = storage.saveRequest(request) val requestId = storage.saveRequest(request)
// Pending request should equals to 1. // Pending request should equals to 1.
assertEquals(1, storage.getPendingRequestIds().size) assertEquals(1, storage.getNewRequestIds().size)
// Certificate should be empty. // Certificate should be empty.
assertEquals(CertificateResponse.NotReady, storage.getResponse(requestId)) assertEquals(CertificateResponse.NotReady, storage.getResponse(requestId))
// Store certificate to DB. // Store certificate to DB.
approveRequest(requestId) val result = storage.approveRequest(requestId)
// Check certificate is stored in DB correctly. // Check request request has been approved
val response = storage.getResponse(requestId) as CertificateResponse.Ready assertTrue(result)
assertThat(response.certificate.publicKey).isEqualTo(keyPair.public) // Check request is not ready yet.
// Pending request should be empty. assertTrue(storage.getResponse(requestId) is CertificateResponse.NotReady)
assertTrue(storage.getPendingRequestIds().isEmpty()) // New request should be empty.
assertTrue(storage.getNewRequestIds().isEmpty())
}
@Test
fun `approve request ignores subsequent approvals`() {
// Given
val (request, _) = createRequest("LegalName")
// Add request to DB.
val requestId = storage.saveRequest(request)
storage.approveRequest(requestId)
// When subsequent approval is performed
val result = storage.approveRequest(requestId)
// Then check request has not been approved
assertFalse(result)
}
@Test
fun `sign request`() {
val (csr, _) = createRequest("LegalName")
// Add request to DB.
val requestId = storage.saveRequest(csr)
// New request should equals to 1.
assertEquals(1, storage.getNewRequestIds().size)
// Certificate should be empty.
assertEquals(CertificateResponse.NotReady, storage.getResponse(requestId))
// Store certificate to DB.
storage.approveRequest(requestId)
// Check request is not ready yet.
assertTrue(storage.getResponse(requestId) is CertificateResponse.NotReady)
// New request should be empty.
assertTrue(storage.getNewRequestIds().isEmpty())
// Sign certificate
storage.signCertificate(requestId) {
JcaPKCS10CertificationRequest(csr.request).run {
val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val rootCACert = X509Utilities.createSelfSignedCACertificate(X500Name("CN=Corda Node Root CA,L=London"), rootCAKey)
val intermediateCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey, X500Name("CN=Corda Node Intermediate CA,L=London"), intermediateCAKey.public)
val ourCertificate = X509Utilities.createCertificate(CertificateType.TLS, intermediateCACert, intermediateCAKey, subject, publicKey).toX509Certificate()
buildCertPath(ourCertificate, intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate())
}
}
// Check request is ready
assertTrue(storage.getResponse(requestId) is CertificateResponse.Ready)
}
@Test
fun `sign request ignores subsequent sign requests`() {
val (csr, _) = createRequest("LegalName")
// Add request to DB.
val requestId = storage.saveRequest(csr)
// Store certificate to DB.
storage.approveRequest(requestId)
val generateCert: CertificationRequestData.() -> CertPath = {
JcaPKCS10CertificationRequest(csr.request).run {
val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val rootCACert = X509Utilities.createSelfSignedCACertificate(X500Name("CN=Corda Node Root CA,L=London"), rootCAKey)
val intermediateCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey, X500Name("CN=Corda Node Intermediate CA,L=London"), intermediateCAKey.public)
val ourCertificate = X509Utilities.createCertificate(CertificateType.TLS, intermediateCACert, intermediateCAKey, subject, publicKey).toX509Certificate()
buildCertPath(ourCertificate, intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate())
}
}
// Sign certificate
storage.signCertificate(requestId, generateCertificate = generateCert)
// When subsequent signature requested
val result = storage.signCertificate(requestId, generateCertificate = generateCert)
// Then check request has not been signed
assertFalse(result)
} }
@Test @Test
fun `reject request`() { fun `reject request`() {
val requestId = storage.saveRequest(createRequest("BankA").first) val requestId = storage.saveRequest(createRequest("BankA").first)
storage.rejectRequest(requestId, "Because I said so!") storage.rejectRequest(requestId, rejectReason = "Because I said so!")
assertThat(storage.getPendingRequestIds()).isEmpty() assertThat(storage.getNewRequestIds()).isEmpty()
val response = storage.getResponse(requestId) as CertificateResponse.Unauthorised val response = storage.getResponse(requestId) as CertificateResponse.Unauthorised
assertThat(response.message).isEqualTo("Because I said so!") assertThat(response.message).isEqualTo("Because I said so!")
} }
@ -85,20 +154,20 @@ class DBCertificateRequestStorageTest {
@Test @Test
fun `request with the same legal name as a pending request`() { fun `request with the same legal name as a pending request`() {
val requestId1 = storage.saveRequest(createRequest("BankA").first) val requestId1 = storage.saveRequest(createRequest("BankA").first)
assertThat(storage.getPendingRequestIds()).containsOnly(requestId1) assertThat(storage.getNewRequestIds()).containsOnly(requestId1)
val requestId2 = storage.saveRequest(createRequest("BankA").first) val requestId2 = storage.saveRequest(createRequest("BankA").first)
assertThat(storage.getPendingRequestIds()).containsOnly(requestId1) assertThat(storage.getNewRequestIds()).containsOnly(requestId1)
val response2 = storage.getResponse(requestId2) as CertificateResponse.Unauthorised val response2 = storage.getResponse(requestId2) as CertificateResponse.Unauthorised
assertThat(response2.message).containsIgnoringCase("duplicate") assertThat(response2.message).containsIgnoringCase("duplicate")
// Make sure the first request is processed properly // Make sure the first request is processed properly
approveRequest(requestId1) storage.approveRequest(requestId1)
assertThat(storage.getResponse(requestId1)).isInstanceOf(CertificateResponse.Ready::class.java) assertThat(storage.getResponse(requestId1)).isInstanceOf(CertificateResponse.NotReady::class.java)
} }
@Test @Test
fun `request with the same legal name as a previously approved request`() { fun `request with the same legal name as a previously approved request`() {
val requestId1 = storage.saveRequest(createRequest("BankA").first) val requestId1 = storage.saveRequest(createRequest("BankA").first)
approveRequest(requestId1) storage.approveRequest(requestId1)
val requestId2 = storage.saveRequest(createRequest("BankA").first) val requestId2 = storage.saveRequest(createRequest("BankA").first)
val response2 = storage.getResponse(requestId2) as CertificateResponse.Unauthorised val response2 = storage.getResponse(requestId2) as CertificateResponse.Unauthorised
assertThat(response2.message).containsIgnoringCase("duplicate") assertThat(response2.message).containsIgnoringCase("duplicate")
@ -107,11 +176,11 @@ class DBCertificateRequestStorageTest {
@Test @Test
fun `request with the same legal name as a previously rejected request`() { fun `request with the same legal name as a previously rejected request`() {
val requestId1 = storage.saveRequest(createRequest("BankA").first) val requestId1 = storage.saveRequest(createRequest("BankA").first)
storage.rejectRequest(requestId1, "Because I said so!") storage.rejectRequest(requestId1, rejectReason = "Because I said so!")
val requestId2 = storage.saveRequest(createRequest("BankA").first) val requestId2 = storage.saveRequest(createRequest("BankA").first)
assertThat(storage.getPendingRequestIds()).containsOnly(requestId2) assertThat(storage.getNewRequestIds()).containsOnly(requestId2)
approveRequest(requestId2) storage.approveRequest(requestId2)
assertThat(storage.getResponse(requestId2)).isInstanceOf(CertificateResponse.Ready::class.java) assertThat(storage.getResponse(requestId2)).isInstanceOf(CertificateResponse.NotReady::class.java)
} }
private fun createRequest(legalName: String): Pair<CertificationRequestData, KeyPair> { private fun createRequest(legalName: String): Pair<CertificationRequestData, KeyPair> {
@ -123,15 +192,6 @@ class DBCertificateRequestStorageTest {
return Pair(request, keyPair) return Pair(request, keyPair)
} }
private fun approveRequest(requestId: String) {
storage.approveRequest(requestId) {
JcaPKCS10CertificationRequest(request).run {
val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, subject))), arrayOf())
X509Utilities.createCertificate(CertificateType.CLIENT_CA, intermediateCACert, intermediateCAKey, subject, publicKey, nameConstraints = nameConstraints).toX509Certificate()
}
}
}
private fun makeTestDataSourceProperties(nodeName: String = SecureHash.randomSHA256().toString()): Properties { private fun makeTestDataSourceProperties(nodeName: String = SecureHash.randomSHA256().toString()): Properties {
val props = Properties() val props = Properties()
props.setProperty("dataSourceClassName", "org.h2.jdbcx.JdbcDataSource") props.setProperty("dataSourceClassName", "org.h2.jdbcx.JdbcDataSource")