mirror of
https://github.com/corda/corda.git
synced 2024-12-29 09:18:58 +00:00
added keygen functionality to doorman
This commit is contained in:
parent
6ff8d73402
commit
011dee09d8
@ -1,14 +1,27 @@
|
|||||||
package com.r3.corda.doorman
|
package com.r3.corda.doorman
|
||||||
|
|
||||||
import com.google.common.net.HostAndPort
|
import com.google.common.net.HostAndPort
|
||||||
|
import com.r3.corda.doorman.OptionParserHelper.toConfigWithOptions
|
||||||
|
import com.r3.corda.doorman.persistence.CertificationRequestStorage
|
||||||
import com.r3.corda.doorman.persistence.DBCertificateRequestStorage
|
import com.r3.corda.doorman.persistence.DBCertificateRequestStorage
|
||||||
import joptsimple.ArgumentAcceptingOptionSpec
|
|
||||||
import joptsimple.OptionParser
|
|
||||||
import net.corda.core.crypto.X509Utilities
|
import net.corda.core.crypto.X509Utilities
|
||||||
import net.corda.core.utilities.debug
|
import net.corda.core.crypto.X509Utilities.CACertAndKey
|
||||||
|
import net.corda.core.crypto.X509Utilities.CORDA_INTERMEDIATE_CA
|
||||||
|
import net.corda.core.crypto.X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY
|
||||||
|
import net.corda.core.crypto.X509Utilities.CORDA_ROOT_CA
|
||||||
|
import net.corda.core.crypto.X509Utilities.CORDA_ROOT_CA_PRIVATE_KEY
|
||||||
|
import net.corda.core.crypto.X509Utilities.addOrReplaceKey
|
||||||
|
import net.corda.core.crypto.X509Utilities.createIntermediateCert
|
||||||
|
import net.corda.core.crypto.X509Utilities.loadCertificateAndKey
|
||||||
|
import net.corda.core.crypto.X509Utilities.loadKeyStore
|
||||||
|
import net.corda.core.crypto.X509Utilities.loadOrCreateKeyStore
|
||||||
|
import net.corda.core.crypto.X509Utilities.saveKeyStore
|
||||||
|
import net.corda.core.div
|
||||||
|
import net.corda.core.seconds
|
||||||
import net.corda.core.utilities.loggerFor
|
import net.corda.core.utilities.loggerFor
|
||||||
import net.corda.node.services.config.ConfigHelper
|
import net.corda.node.services.config.ConfigHelper
|
||||||
import net.corda.node.services.config.getProperties
|
import net.corda.node.services.config.getOrElse
|
||||||
|
import net.corda.node.services.config.getValue
|
||||||
import net.corda.node.utilities.configureDatabase
|
import net.corda.node.utilities.configureDatabase
|
||||||
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
|
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
|
||||||
import org.eclipse.jetty.server.Server
|
import org.eclipse.jetty.server.Server
|
||||||
@ -19,8 +32,12 @@ import org.eclipse.jetty.servlet.ServletHolder
|
|||||||
import org.glassfish.jersey.server.ResourceConfig
|
import org.glassfish.jersey.server.ResourceConfig
|
||||||
import org.glassfish.jersey.servlet.ServletContainer
|
import org.glassfish.jersey.servlet.ServletContainer
|
||||||
import java.io.Closeable
|
import java.io.Closeable
|
||||||
|
import java.lang.Thread.sleep
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
|
import java.nio.file.Path
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
|
import java.security.cert.Certificate
|
||||||
|
import java.util.*
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
@ -29,12 +46,17 @@ 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(val webServerAddr: HostAndPort, val doormanWebService: DoormanWebService) : Closeable {
|
class DoormanServer(webServerAddr: HostAndPort, val caCertAndKey: CACertAndKey, val rootCACert: Certificate, val storage: CertificationRequestStorage) : Closeable {
|
||||||
companion object {
|
companion object {
|
||||||
val log = loggerFor<DoormanServer>()
|
val log = loggerFor<DoormanServer>()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val server: Server = initWebServer()
|
val server: Server = Server(InetSocketAddress(webServerAddr.hostText, webServerAddr.port)).apply {
|
||||||
|
server.handler = HandlerCollection().apply {
|
||||||
|
addHandler(buildServletContextHandler())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val hostAndPort: HostAndPort get() = server.connectors
|
val hostAndPort: HostAndPort get() = server.connectors
|
||||||
.map { it as? ServerConnector }
|
.map { it as? ServerConnector }
|
||||||
.filterNotNull()
|
.filterNotNull()
|
||||||
@ -42,20 +64,15 @@ class DoormanServer(val webServerAddr: HostAndPort, val doormanWebService: Doorm
|
|||||||
.first()
|
.first()
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
log.info("Shutting down CertificateSigningService...")
|
log.info("Shutting down Doorman Web Services...")
|
||||||
server.stop()
|
server.stop()
|
||||||
server.join()
|
server.join()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initWebServer(): Server {
|
fun start() {
|
||||||
return Server(InetSocketAddress(webServerAddr.hostText, webServerAddr.port)).apply {
|
log.info("Starting Doorman Web Services...")
|
||||||
log.info("Starting CertificateSigningService...")
|
server.start()
|
||||||
handler = HandlerCollection().apply {
|
log.info("Doorman Web Services started on $hostAndPort")
|
||||||
addHandler(buildServletContextHandler())
|
|
||||||
}
|
|
||||||
start()
|
|
||||||
log.info("CertificateSigningService started on $hostAndPort")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildServletContextHandler(): ServletContextHandler {
|
private fun buildServletContextHandler(): ServletContextHandler {
|
||||||
@ -63,7 +80,7 @@ class DoormanServer(val webServerAddr: HostAndPort, val doormanWebService: Doorm
|
|||||||
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)
|
register(DoormanWebService(caCertAndKey, rootCACert, storage))
|
||||||
}
|
}
|
||||||
val jerseyServlet = ServletHolder(ServletContainer(resourceConfig)).apply {
|
val jerseyServlet = ServletHolder(ServletContainer(resourceConfig)).apply {
|
||||||
initOrder = 0 // Initialise at server start
|
initOrder = 0 // Initialise at server start
|
||||||
@ -73,62 +90,138 @@ class DoormanServer(val webServerAddr: HostAndPort, val doormanWebService: Doorm
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object ParamsSpec {
|
class DoormanParameters(args: Array<String>) {
|
||||||
val parser = OptionParser()
|
private val argConfig = args.toConfigWithOptions {
|
||||||
val basedir: ArgumentAcceptingOptionSpec<String>? = parser.accepts("basedir", "Overriding configuration file path.")
|
accepts("basedir", "Overriding configuration filepath, default to current directory.").withRequiredArg().describedAs("filepath")
|
||||||
.withRequiredArg()
|
accepts("keygen", "Generate CA keypair and certificate using provide Root CA key.").withOptionalArg()
|
||||||
|
accepts("rootKeygen", "Generate Root CA keypair and certificate.").withOptionalArg()
|
||||||
|
accepts("approveAll", "Approve all certificate signing request.").withOptionalArg()
|
||||||
|
accepts("keystorePath", "CA keystore filepath, default to [basedir]/certificates/caKeystore.jks.").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("caPrivateKeyPassword", "CA private key password.").withRequiredArg().describedAs("password")
|
||||||
|
accepts("rootKeystorePassword", "Root CA keystore password.").withRequiredArg().describedAs("password")
|
||||||
|
accepts("rootPrivateKeyPassword", "Root private key password.").withRequiredArg().describedAs("password")
|
||||||
|
accepts("host", "Doorman web service host override").withRequiredArg().describedAs("hostname")
|
||||||
|
accepts("port", "Doorman web service port override").withRequiredArg().ofType(Int::class.java).describedAs("port number")
|
||||||
|
}
|
||||||
|
private val basedir by argConfig.getOrElse { Paths.get(".") }
|
||||||
|
private val config = argConfig.withFallback(ConfigHelper.loadConfig(basedir, allowMissingConfig = true))
|
||||||
|
val keystorePath: Path by config.getOrElse { basedir / "certificates" / "caKeystore.jks" }
|
||||||
|
val rootStorePath: Path by config.getOrElse { basedir / "certificates" / "rootCAKeystore.jks" }
|
||||||
|
val keystorePassword: String? by config.getOrElse { null }
|
||||||
|
val caPrivateKeyPassword: String? by config.getOrElse { null }
|
||||||
|
val rootKeystorePassword: String? by config.getOrElse { null }
|
||||||
|
val rootPrivateKeyPassword: String? by config.getOrElse { null }
|
||||||
|
val approveAll: Boolean by config
|
||||||
|
val host: String by config
|
||||||
|
val port: Int by config
|
||||||
|
val dataSourceProperties: Properties by config
|
||||||
|
private val keygen: Boolean by config.getOrElse { false }
|
||||||
|
private val rootKeygen: Boolean by config.getOrElse { false }
|
||||||
|
|
||||||
|
val mode = if (rootKeygen) Mode.ROOT_KEYGEN else if (keygen) Mode.CA_KEYGEN else Mode.DOORMAN
|
||||||
|
|
||||||
|
enum class Mode {
|
||||||
|
DOORMAN, CA_KEYGEN, ROOT_KEYGEN
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun main(args: Array<String>) {
|
fun main(args: Array<String>) {
|
||||||
|
fun readPassword(fmt: String): String {
|
||||||
|
return if (System.console() != null) {
|
||||||
|
String(System.console().readPassword(fmt))
|
||||||
|
} else {
|
||||||
|
print(fmt)
|
||||||
|
readLine()!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DoormanParameters(args).run {
|
||||||
val log = DoormanServer.log
|
val log = DoormanServer.log
|
||||||
log.info("Starting certificate signing server.")
|
when (mode) {
|
||||||
try {
|
DoormanParameters.Mode.ROOT_KEYGEN -> {
|
||||||
ParamsSpec.parser.parse(*args)
|
println("Generating Root CA keypair and certificate.")
|
||||||
} catch (ex: Exception) {
|
val rootKeystorePassword = rootKeystorePassword ?: readPassword("Root Keystore Password : ")
|
||||||
log.error("Unable to parse args", ex)
|
val rootStore = loadOrCreateKeyStore(rootStorePath, rootKeystorePassword)
|
||||||
ParamsSpec.parser.printHelpOn(System.out)
|
val rootPrivateKeyPassword = rootPrivateKeyPassword ?: readPassword("Root Private Key Password : ")
|
||||||
exitProcess(1)
|
|
||||||
}.run {
|
|
||||||
val basedir = Paths.get(valueOf(ParamsSpec.basedir) ?: ".")
|
|
||||||
val config = ConfigHelper.loadConfig(basedir)
|
|
||||||
|
|
||||||
val keystore = X509Utilities.loadKeyStore(Paths.get(config.getString("keystorePath")).normalize(), config.getString("keyStorePassword"))
|
if (rootStore.containsAlias(CORDA_ROOT_CA_PRIVATE_KEY)) {
|
||||||
val intermediateCACertAndKey = X509Utilities.loadCertificateAndKey(keystore, config.getString("caKeyPassword"), X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY)
|
val oldKey = loadOrCreateKeyStore(rootStorePath, rootKeystorePassword).getCertificate(CORDA_ROOT_CA_PRIVATE_KEY).publicKey
|
||||||
val rootCA = keystore.getCertificateChain(X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY).last()
|
println("Key $CORDA_ROOT_CA_PRIVATE_KEY already exists in keystore, process will now terminate.")
|
||||||
|
println(oldKey)
|
||||||
|
exitProcess(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
val selfSignCert = X509Utilities.createSelfSignedCACert(CORDA_ROOT_CA)
|
||||||
|
rootStore.addOrReplaceKey(CORDA_ROOT_CA_PRIVATE_KEY, selfSignCert.keyPair.private, rootPrivateKeyPassword.toCharArray(), arrayOf(selfSignCert.certificate))
|
||||||
|
saveKeyStore(rootStore, rootStorePath, rootKeystorePassword)
|
||||||
|
|
||||||
|
println("Root CA keypair and certificate stored in $rootStorePath.")
|
||||||
|
println(loadKeyStore(rootStorePath, rootKeystorePassword).getCertificate(CORDA_ROOT_CA_PRIVATE_KEY).publicKey)
|
||||||
|
}
|
||||||
|
DoormanParameters.Mode.CA_KEYGEN -> {
|
||||||
|
println("Generating Intermediate CA keypair and certificate.")
|
||||||
|
val rootKeystorePassword = rootKeystorePassword ?: readPassword("Root Keystore Password : ")
|
||||||
|
val rootPrivateKeyPassword = rootPrivateKeyPassword ?: readPassword("Root Private Key Password : ")
|
||||||
|
val rootKeyStore = loadKeyStore(rootStorePath, rootKeystorePassword)
|
||||||
|
|
||||||
|
val rootKeyAndCert = loadCertificateAndKey(rootKeyStore, rootPrivateKeyPassword, CORDA_ROOT_CA_PRIVATE_KEY)
|
||||||
|
|
||||||
|
val keystorePassword = keystorePassword ?: readPassword("Keystore Password : ")
|
||||||
|
val caPrivateKeyPassword = caPrivateKeyPassword ?: readPassword("CA Private Key Password : ")
|
||||||
|
val keyStore = loadOrCreateKeyStore(keystorePath, keystorePassword)
|
||||||
|
|
||||||
|
if (keyStore.containsAlias(CORDA_INTERMEDIATE_CA_PRIVATE_KEY)) {
|
||||||
|
val oldKey = loadOrCreateKeyStore(keystorePath, rootKeystorePassword).getCertificate(CORDA_INTERMEDIATE_CA_PRIVATE_KEY).publicKey
|
||||||
|
println("Key $CORDA_INTERMEDIATE_CA_PRIVATE_KEY already exists in keystore, process will now terminate.")
|
||||||
|
println(oldKey)
|
||||||
|
exitProcess(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
val intermediateKeyAndCert = createIntermediateCert(CORDA_INTERMEDIATE_CA, rootKeyAndCert)
|
||||||
|
keyStore.addOrReplaceKey(CORDA_INTERMEDIATE_CA_PRIVATE_KEY, intermediateKeyAndCert.keyPair.private,
|
||||||
|
caPrivateKeyPassword.toCharArray(), arrayOf(intermediateKeyAndCert.certificate, rootKeyAndCert.certificate))
|
||||||
|
saveKeyStore(keyStore, keystorePath, keystorePassword)
|
||||||
|
println("Intermediate CA keypair and certificate stored in $keystorePath.")
|
||||||
|
println(loadKeyStore(keystorePath, keystorePassword).getCertificate(CORDA_INTERMEDIATE_CA_PRIVATE_KEY).publicKey)
|
||||||
|
}
|
||||||
|
DoormanParameters.Mode.DOORMAN -> {
|
||||||
|
log.info("Starting certificate signing server.")
|
||||||
|
|
||||||
|
val keystorePassword = keystorePassword ?: readPassword("Keystore Password : ")
|
||||||
|
val caPrivateKeyPassword = caPrivateKeyPassword ?: readPassword("CA Private Key Password : ")
|
||||||
|
|
||||||
|
val keystore = X509Utilities.loadKeyStore(keystorePath, keystorePassword)
|
||||||
|
val rootCACert = keystore.getCertificateChain(CORDA_INTERMEDIATE_CA_PRIVATE_KEY).last()
|
||||||
|
val caCertAndKey = X509Utilities.loadCertificateAndKey(keystore, caPrivateKeyPassword, CORDA_INTERMEDIATE_CA_PRIVATE_KEY)
|
||||||
|
|
||||||
// Create DB connection.
|
// Create DB connection.
|
||||||
val (datasource, database) = configureDatabase(config.getProperties("dataSourceProperties"))
|
val (datasource, database) = configureDatabase(dataSourceProperties)
|
||||||
val storage = DBCertificateRequestStorage(database)
|
val storage = DBCertificateRequestStorage(database)
|
||||||
val service = DoormanWebService(intermediateCACertAndKey, rootCA, storage)
|
// Daemon thread approving all request periodically.
|
||||||
|
val approvalThread = if (approveAll) {
|
||||||
// Background thread approving all request periodically.
|
thread(name = "Request Approval Daemon") {
|
||||||
var stopSigner = false
|
while (true) {
|
||||||
val certSinger = if (config.getBoolean("approveAll")) {
|
sleep(10.seconds.toMillis())
|
||||||
thread {
|
|
||||||
while (!stopSigner) {
|
|
||||||
Thread.sleep(1000)
|
|
||||||
for (id in storage.getPendingRequestIds()) {
|
for (id in storage.getPendingRequestIds()) {
|
||||||
storage.approveRequest(id, {
|
storage.approveRequest(id, {
|
||||||
JcaPKCS10CertificationRequest(it.request).run {
|
JcaPKCS10CertificationRequest(it.request).run {
|
||||||
X509Utilities.createServerCert(subject, publicKey, intermediateCACertAndKey,
|
X509Utilities.createServerCert(subject, publicKey, caCertAndKey,
|
||||||
if (it.ipAddress == it.hostName) listOf() else listOf(it.hostName), listOf(it.ipAddress))
|
if (it.ipAddress == it.hostName) listOf() else listOf(it.hostName), listOf(it.ipAddress))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
log.debug { "Approved $id" }
|
log.info("Approved $id")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.debug { "Certificate Signer thread stopped." }
|
}
|
||||||
}
|
} else null
|
||||||
} else {
|
DoormanServer(HostAndPort.fromParts(host, port), caCertAndKey, rootCACert, storage).use {
|
||||||
null
|
it.start()
|
||||||
}
|
it.server.join()
|
||||||
|
approvalThread?.interrupt()
|
||||||
DoormanServer(HostAndPort.fromParts(config.getString("host"), config.getInt("port")), service).use {
|
approvalThread?.join()
|
||||||
Runtime.getRuntime().addShutdownHook(thread(false) {
|
}
|
||||||
stopSigner = true
|
}
|
||||||
certSinger?.join()
|
|
||||||
datasource.close()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
package com.r3.corda.doorman
|
||||||
|
|
||||||
|
import com.typesafe.config.Config
|
||||||
|
import com.typesafe.config.ConfigFactory
|
||||||
|
import joptsimple.ArgumentAcceptingOptionSpec
|
||||||
|
import joptsimple.OptionParser
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
object OptionParserHelper {
|
||||||
|
fun Array<String>.toConfigWithOptions(options: OptionParser.() -> Unit): Config {
|
||||||
|
val parser = OptionParser()
|
||||||
|
val helpOption = parser.acceptsAll(listOf("h", "?", "help"), "show help").forHelp();
|
||||||
|
options(parser)
|
||||||
|
val optionSet = parser.parse(*this)
|
||||||
|
if (optionSet.has(helpOption)) {
|
||||||
|
parser.printHelpOn(System.out)
|
||||||
|
exitProcess(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ConfigFactory.parseMap(parser.recognizedOptions().mapValues {
|
||||||
|
val optionSpec = it.value
|
||||||
|
if (optionSpec is ArgumentAcceptingOptionSpec<*> && !optionSpec.requiresArgument() && optionSet.has(optionSpec)) true else optionSpec.value(optionSet)
|
||||||
|
}.filterValues { it != null })
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,8 @@
|
|||||||
host = localhost
|
host = localhost
|
||||||
port = 0
|
port = 0
|
||||||
keystorePath = ${basedir}"/certificates/keystore.jks"
|
keystorePath = ${basedir}"/certificates/caKeystore.jks"
|
||||||
keyStorePassword = "password"
|
keystorePassword = "password"
|
||||||
caKeyPassword = "password"
|
caPrivateKeyPassword = "password"
|
||||||
approveAll = true
|
approveAll = true
|
||||||
|
|
||||||
dataSourceProperties {
|
dataSourceProperties {
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
package com.r3.corda.doorman
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class DoormanParametersTest {
|
||||||
|
@Test
|
||||||
|
fun `parse arg correctly`() {
|
||||||
|
val params = DoormanParameters(arrayOf("--keygen", "--keystorePath", "./testDummyPath.jks", "--approveAll"))
|
||||||
|
assertEquals(DoormanParameters.Mode.CA_KEYGEN, params.mode)
|
||||||
|
assertEquals("./testDummyPath.jks", params.keystorePath.toString())
|
||||||
|
assertEquals(0, params.port)
|
||||||
|
assertTrue(params.approveAll)
|
||||||
|
|
||||||
|
val params2 = DoormanParameters(arrayOf("--keystorePath", "./testDummyPath.jks", "--port", "1000"))
|
||||||
|
assertEquals(DoormanParameters.Mode.DOORMAN, params2.mode)
|
||||||
|
assertEquals("./testDummyPath.jks", params2.keystorePath.toString())
|
||||||
|
assertEquals(1000, params2.port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -31,7 +31,8 @@ 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), DoormanWebService(intermediateCA, rootCA.certificate, storage))
|
doormanServer = DoormanServer(HostAndPort.fromParts("localhost", 0), intermediateCA, rootCA.certificate, storage)
|
||||||
|
doormanServer.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
|
Loading…
Reference in New Issue
Block a user