mirror of
https://github.com/corda/corda.git
synced 2025-01-16 01:40:17 +00:00
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:
parent
8376f29cac
commit
34c0f6b89c
@ -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")
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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())
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
||||||
}
|
}
|
@ -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))
|
||||||
}
|
}
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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")
|
||||||
|
Loading…
Reference in New Issue
Block a user