diff --git a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/doorman/NodeRegistrationTest.kt b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/doorman/NodeRegistrationTest.kt index 4ece2a3a21..6fa65c1787 100644 --- a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/doorman/NodeRegistrationTest.kt +++ b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/doorman/NodeRegistrationTest.kt @@ -1,6 +1,7 @@ package com.r3.corda.networkmanage.doorman import com.r3.corda.networkmanage.common.persistence.configureDatabase +import com.r3.corda.networkmanage.common.utils.CertPathAndKey import com.r3.corda.networkmanage.doorman.signer.LocalSigner import net.corda.cordform.CordformNode import net.corda.core.crypto.random63BitValue @@ -141,11 +142,11 @@ class NodeRegistrationTest : IntegrationTest() { start( serverAddress, configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(runMigration = true)), - LocalSigner(csrCa.keyPair, arrayOf(csrCa.certificate, rootCaCert)), + CertPathAndKey(listOf(csrCa.certificate, rootCaCert), csrCa.keyPair.private), DoormanConfig(approveAll = true, jiraConfig = null, approveInterval = timeoutMillis), networkParameters?.let { NetworkMapStartParams( - LocalSigner(networkMapCa.keyPair, arrayOf(networkMapCa.certificate, rootCaCert)), + LocalSigner(networkMapCa), networkParameters, NetworkMapConfig(cacheTimeout = timeoutMillis, signInterval = timeoutMillis) ) diff --git a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/HsmTest.kt b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/HsmTest.kt index 33c9cbef3a..c78f1ca852 100644 --- a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/HsmTest.kt +++ b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/HsmTest.kt @@ -12,6 +12,7 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder +import java.util.concurrent.atomic.AtomicBoolean import kotlin.test.assertTrue class HsmTest { @@ -19,7 +20,12 @@ class HsmTest { @Rule @JvmField val hsmSimulator: HsmSimulator = HsmSimulator() - val testParameters = Parameters( + + @Rule + @JvmField + val tempFolder = TemporaryFolder() + + private val testParameters = Parameters( dataSourceProperties = mock(), device = "${hsmSimulator.port}@${hsmSimulator.host}", keySpecifier = 1, @@ -30,10 +36,6 @@ class HsmTest { validDays = 3650 ) - @Rule - @JvmField - val tempFolder = TemporaryFolder() - private lateinit var inputReader: InputReader @Before @@ -47,14 +49,12 @@ class HsmTest { fun `Authenticator executes the block once user is successfully authenticated`() { // given val authenticator = Authenticator(testParameters.createProvider(), inputReader = inputReader) - var executed = false + val executed = AtomicBoolean(false) // when - authenticator.connectAndAuthenticate({ provider, signers -> - executed = true - }) + authenticator.connectAndAuthenticate { _, _ -> executed.set(true) } // then - assertTrue(executed) + assertTrue(executed.get()) } } \ No newline at end of file diff --git a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/SigningServiceIntegrationTest.kt b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/SigningServiceIntegrationTest.kt index 765d47d5f0..8c4a3b8215 100644 --- a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/SigningServiceIntegrationTest.kt +++ b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/SigningServiceIntegrationTest.kt @@ -90,7 +90,12 @@ class SigningServiceIntegrationTest { val database = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(runMigration = true)) NetworkManagementServer().use { server -> - server.start(NetworkHostAndPort(HOST, 0), database, doormanServiceParameter = DoormanConfig(approveAll = true, approveInterval = 2.seconds.toMillis(), jiraConfig = null), startNetworkMap = null) + server.start( + hostAndPort = NetworkHostAndPort(HOST, 0), + database = database, + csrCertPathAndKey = null, + doormanServiceParameter = DoormanConfig(approveAll = true, approveInterval = 2.seconds.toMillis(), jiraConfig = null), + startNetworkMap = null) val doormanHostAndPort = server.hostAndPort // Start Corda network registration. val config = createConfig().also { diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/NetworkMapStorage.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/NetworkMapStorage.kt index cb86c9af6f..59c83b0abc 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/NetworkMapStorage.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/NetworkMapStorage.kt @@ -39,10 +39,9 @@ interface NetworkMapStorage { fun getSignedNetworkParameters(hash: SecureHash): SignedNetworkParameters? /** - * Retrieve network map parameters. - * @return signed current network map parameters or null if they don't exist + * Retrieve the network parameters of the current network map, or null if there's no network map. */ - fun getCurrentSignedNetworkParameters(): SignedNetworkParameters? + fun getNetworkParametersOfNetworkMap(): SignedNetworkParameters? /** * Persists given network parameters with signature if provided. @@ -55,5 +54,5 @@ interface NetworkMapStorage { * Note that they may not have been signed up yet. * @return latest network parameters */ - fun getLatestUnsignedNetworkParameters(): NetworkParameters -} \ No newline at end of file + fun getLatestNetworkParameters(): NetworkParameters? +} diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNetworkMapStorage.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNetworkMapStorage.kt index 0e328f853a..de7fed484b 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNetworkMapStorage.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNetworkMapStorage.kt @@ -6,10 +6,7 @@ import com.r3.corda.networkmanage.common.utils.SignedNetworkParameters import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sha256 import net.corda.core.internal.DigitalSignatureWithCert -import net.corda.core.serialization.SerializedBytes -import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize -import net.corda.nodeapi.internal.network.NetworkMap import net.corda.nodeapi.internal.network.NetworkParameters import net.corda.nodeapi.internal.persistence.CordaPersistence @@ -19,18 +16,16 @@ import net.corda.nodeapi.internal.persistence.CordaPersistence class PersistentNetworkMapStorage(private val database: CordaPersistence) : NetworkMapStorage { override fun getCurrentNetworkMap(): SignedNetworkMap? { return database.transaction { - getCurrentNetworkMapEntity()?.let { - val signatureAndCertPath = it.signatureAndCertificate() - SignedNetworkMap(SerializedBytes(it.networkMap), signatureAndCertPath) - } + getCurrentNetworkMapEntity()?.toSignedNetworkMap() } } - override fun getCurrentSignedNetworkParameters(): SignedNetworkParameters? { + override fun getNetworkParametersOfNetworkMap(): SignedNetworkParameters? { return database.transaction { getCurrentNetworkMapEntity()?.let { - val netParamsHash = it.networkMap.deserialize().networkParameterHash - getSignedNetworkParameters(netParamsHash) + val netParamsHash = it.toNetworkMap().networkParameterHash + getSignedNetworkParameters(netParamsHash) ?: + throw IllegalStateException("Current network map is pointing to network parameters that do not exist: $netParamsHash") } } } @@ -47,7 +42,9 @@ class PersistentNetworkMapStorage(private val database: CordaPersistence) : Netw } override fun getSignedNetworkParameters(hash: SecureHash): SignedNetworkParameters? { - return getNetworkParametersEntity(hash.toString())?.signedParameters() + return getNetworkParametersEntity(hash.toString())?.let { + if (it.isSigned) it.toSignedNetworkParameters() else null + } } override fun getNodeInfoHashes(certificateStatus: CertificateStatus): List { @@ -79,18 +76,17 @@ class PersistentNetworkMapStorage(private val database: CordaPersistence) : Netw } } - override fun getLatestUnsignedNetworkParameters(): NetworkParameters = getLatestNetworkParametersEntity().networkParameters() - - private fun getLatestNetworkParametersEntity(): NetworkParametersEntity { + override fun getLatestNetworkParameters(): NetworkParameters? { return database.transaction { - val builder = session.criteriaBuilder - val query = builder.createQuery(NetworkParametersEntity::class.java).run { - from(NetworkParametersEntity::class.java).run { - orderBy(builder.desc(get(NetworkParametersEntity::created.name))) + val query = session.criteriaBuilder.run { + createQuery(NetworkParametersEntity::class.java).run { + from(NetworkParametersEntity::class.java).run { + orderBy(desc(get(NetworkParametersEntity::created.name))) + } } } // We just want the last entry - session.createQuery(query).setMaxResults(1).resultList.singleOrNull() ?: throw IllegalArgumentException("No network parameters found in network map storage") + session.createQuery(query).setMaxResults(1).uniqueResult()?.toNetworkParameters() } } diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/NetworkMapEntity.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/NetworkMapEntity.kt index f71a252213..65c14f560a 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/NetworkMapEntity.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/NetworkMapEntity.kt @@ -1,7 +1,11 @@ package com.r3.corda.networkmanage.common.persistence.entity +import com.r3.corda.networkmanage.common.utils.SignedNetworkMap import net.corda.core.internal.DigitalSignatureWithCert +import net.corda.core.serialization.SerializedBytes +import net.corda.core.serialization.deserialize import net.corda.nodeapi.internal.crypto.X509CertificateFactory +import net.corda.nodeapi.internal.network.NetworkMap import javax.persistence.* @Entity @@ -23,11 +27,12 @@ class NetworkMapEntity( @Column(name = "certificate") val certificate: ByteArray ) { - /** - * Deserializes NetworkMapEntity.signatureBytes into the [DigitalSignatureWithCert] instance - */ - fun signatureAndCertificate(): DigitalSignatureWithCert { - return DigitalSignatureWithCert(X509CertificateFactory().generateCertificate(certificate.inputStream()), signature) - } + fun toNetworkMap(): NetworkMap = networkMap.deserialize() -} \ No newline at end of file + fun toSignedNetworkMap(): SignedNetworkMap { + return SignedNetworkMap( + SerializedBytes(networkMap), + DigitalSignatureWithCert(X509CertificateFactory().generateCertificate(certificate.inputStream()), signature) + ) + } +} diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/NetworkParametersEntity.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/NetworkParametersEntity.kt index ca445a1f02..8cd860486c 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/NetworkParametersEntity.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/NetworkParametersEntity.kt @@ -35,13 +35,15 @@ class NetworkParametersEntity( @Column(name = "certificate") val certificate: ByteArray? ) { - fun networkParameters(): NetworkParameters = parametersBytes.deserialize() + val isSigned: Boolean get() = certificate != null && signature != null - // Return signed network parameters or null if they haven't been signed yet. - fun signedParameters(): SignedNetworkParameters? { - return if (certificate != null && signature != null) { - val sigWithCert = DigitalSignatureWithCert(X509CertificateFactory().generateCertificate(certificate.inputStream()), signature) - SignedDataWithCert(SerializedBytes(parametersBytes), sigWithCert) - } else null + fun toNetworkParameters(): NetworkParameters = parametersBytes.deserialize() + + fun toSignedNetworkParameters(): SignedNetworkParameters { + if (certificate == null || signature == null) throw IllegalStateException("Network parameters entity is not signed: $parametersHash") + return SignedDataWithCert( + SerializedBytes(parametersBytes), + DigitalSignatureWithCert(X509CertificateFactory().generateCertificate(certificate.inputStream()), signature) + ) } -} \ No newline at end of file +} diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/signer/NetworkMapSigner.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/signer/NetworkMapSigner.kt index 5d3d3b15e8..e75baa4c5b 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/signer/NetworkMapSigner.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/signer/NetworkMapSigner.kt @@ -4,20 +4,30 @@ import com.r3.corda.networkmanage.common.persistence.CertificateStatus import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage import net.corda.core.internal.SignedDataWithCert import net.corda.core.serialization.serialize +import net.corda.core.utilities.contextLogger import net.corda.nodeapi.internal.network.NetworkMap import net.corda.nodeapi.internal.network.NetworkParameters class NetworkMapSigner(private val networkMapStorage: NetworkMapStorage, private val signer: Signer) { + private companion object { + val logger = contextLogger() + } + /** * Signs the network map and latest network parameters if they haven't been signed yet. */ fun signNetworkMap() { // TODO There is no network parameters update process in place yet. We assume that latest parameters are to be used // in current network map. - val latestNetworkParameters = networkMapStorage.getLatestUnsignedNetworkParameters() - val currentNetworkParameters = networkMapStorage.getCurrentSignedNetworkParameters() - if (currentNetworkParameters?.verified() != latestNetworkParameters) - signNetworkParameters(latestNetworkParameters) + val latestNetworkParameters = networkMapStorage.getLatestNetworkParameters() + if (latestNetworkParameters == null) { + logger.debug("No network parameters present") + return + } + val currentNetworkParameters = networkMapStorage.getNetworkParametersOfNetworkMap() + if (currentNetworkParameters?.verified() != latestNetworkParameters) { + persistSignedNetworkParameters(latestNetworkParameters) + } val currentSignedNetworkMap = networkMapStorage.getCurrentNetworkMap() val nodeInfoHashes = networkMapStorage.getNodeInfoHashes(CertificateStatus.VALID) val serialisedNetworkMap = NetworkMap(nodeInfoHashes, latestNetworkParameters.serialize().hash).serialize() @@ -27,10 +37,8 @@ class NetworkMapSigner(private val networkMapStorage: NetworkMapStorage, private } } - /** - * Signs latest inserted network parameters. - */ - fun signNetworkParameters(networkParameters: NetworkParameters) { + fun persistSignedNetworkParameters(networkParameters: NetworkParameters) { + logger.info("Signing and persisting network parameters: $networkParameters") val digitalSignature = signer.signObject(networkParameters).sig networkMapStorage.saveNetworkParameters(networkParameters, digitalSignature) } diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/utils/Utils.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/utils/Utils.kt index 8ac987e03a..3554580976 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/utils/Utils.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/utils/Utils.kt @@ -10,14 +10,20 @@ import net.corda.core.internal.SignedDataWithCert import net.corda.nodeapi.internal.crypto.X509CertificateFactory import net.corda.nodeapi.internal.network.NetworkMap import net.corda.nodeapi.internal.network.NetworkParameters +import java.security.KeyPair +import java.security.PrivateKey import java.security.PublicKey import java.security.cert.CertPath -import java.security.cert.Certificate +import java.security.cert.X509Certificate // TODO These should be defined in node-api typealias SignedNetworkParameters = SignedDataWithCert typealias SignedNetworkMap = SignedDataWithCert +data class CertPathAndKey(val certPath: List, val key: PrivateKey) { + fun toKeyPair(): KeyPair = KeyPair(certPath[0].publicKey, key) +} + // TODO: replace this with Crypto.hash when its available. /** * Returns SHA256 hash of this public key @@ -42,7 +48,7 @@ fun Array.toConfigWithOptions(registerOptions: OptionParser.() -> Un class ShowHelpException(val parser: OptionParser, val errorMessage: String? = null) : Exception() -fun buildCertPath(vararg certificates: Certificate): CertPath = X509CertificateFactory().delegate.generateCertPath(certificates.asList()) +fun buildCertPath(vararg certificates: X509Certificate): CertPath = X509CertificateFactory().generateCertPath(certificates.asList()) fun buildCertPath(certPathBytes: ByteArray): CertPath = X509CertificateFactory().delegate.generateCertPath(certPathBytes.inputStream()) diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/Main.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/Main.kt index 74fe5885b0..c7dd3f0271 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/Main.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/Main.kt @@ -1,16 +1,11 @@ package com.r3.corda.networkmanage.doorman -import com.atlassian.jira.rest.client.internal.async.AsynchronousJiraRestClientFactory -import com.r3.corda.networkmanage.common.persistence.* +import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage.Companion.DOORMAN_SIGNATURE -import com.r3.corda.networkmanage.common.signer.NetworkMapSigner +import com.r3.corda.networkmanage.common.persistence.configureDatabase +import com.r3.corda.networkmanage.common.utils.CertPathAndKey import com.r3.corda.networkmanage.common.utils.ShowHelpException -import com.r3.corda.networkmanage.doorman.signer.DefaultCsrHandler -import com.r3.corda.networkmanage.doorman.signer.JiraCsrHandler import com.r3.corda.networkmanage.doorman.signer.LocalSigner -import com.r3.corda.networkmanage.doorman.webservice.MonitoringWebService -import com.r3.corda.networkmanage.doorman.webservice.NodeInfoWebService -import com.r3.corda.networkmanage.doorman.webservice.RegistrationWebService import com.r3.corda.networkmanage.hsm.configuration.Parameters.Companion.DEFAULT_CSR_CERTIFICATE_NAME import com.r3.corda.networkmanage.hsm.configuration.Parameters.Companion.DEFAULT_NETWORK_MAP_CERTIFICATE_NAME import net.corda.core.crypto.Crypto @@ -21,141 +16,19 @@ import net.corda.core.internal.div import net.corda.core.serialization.internal.SerializationEnvironmentImpl import net.corda.core.serialization.internal.nodeSerializationEnv import net.corda.core.utilities.NetworkHostAndPort -import net.corda.core.utilities.loggerFor import net.corda.nodeapi.internal.crypto.* import net.corda.nodeapi.internal.network.NetworkParameters -import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.serialization.AMQP_P2P_CONTEXT import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl import net.corda.nodeapi.internal.serialization.amqp.AMQPClientSerializationScheme import org.bouncycastle.pkcs.PKCS10CertificationRequest -import java.io.Closeable -import java.net.URI import java.nio.file.Path import java.security.cert.X509Certificate import java.time.Instant -import java.util.* -import java.util.concurrent.Executors -import java.util.concurrent.TimeUnit import javax.security.auth.x500.X500Principal import kotlin.concurrent.thread import kotlin.system.exitProcess -class NetworkManagementServer : Closeable { - private val doOnClose = mutableListOf<() -> Unit>() - lateinit var hostAndPort: NetworkHostAndPort - - override fun close() = doOnClose.forEach { it() } - - companion object { - private val logger = loggerFor() - } - - private fun getNetworkMapService(config: NetworkMapConfig, database: CordaPersistence, signer: LocalSigner?, updateNetworkParameters: NetworkParameters?): NodeInfoWebService { - val networkMapStorage = PersistentNetworkMapStorage(database) - val nodeInfoStorage = PersistentNodeInfoStorage(database) - val localNetworkMapSigner = if (signer != null) NetworkMapSigner(networkMapStorage, signer) else null - - updateNetworkParameters?.let { - // Persisting new network parameters - val currentNetworkParameters = networkMapStorage.getCurrentSignedNetworkParameters() - if (currentNetworkParameters == null) { - localNetworkMapSigner?.signNetworkParameters(it) ?: networkMapStorage.saveNetworkParameters(it, null) - } else { - throw UnsupportedOperationException("Network parameters already exist. Updating them via the file config is not supported yet.") - } - } - - // This call will fail if parameter is null in DB. - try { - val latestParameter = networkMapStorage.getLatestUnsignedNetworkParameters() - logger.info("Starting network map service with network parameters : $latestParameter") - } catch (e: NoSuchElementException) { - logger.error("No network parameter found, please upload new network parameter before starting network map service. The server will now exit.") - exitProcess(-1) - } - - // Thread sign network map in case of change (i.e. a new node info has been added or a node info has been removed). - if (localNetworkMapSigner != null) { - val scheduledExecutor = Executors.newScheduledThreadPool(1) - val signingThread = Runnable { - try { - localNetworkMapSigner.signNetworkMap() - } catch (e: Exception) { - // Log the error and carry on. - logger.error("Error encountered when processing node info changes.", e) - } - } - scheduledExecutor.scheduleAtFixedRate(signingThread, config.signInterval, config.signInterval, TimeUnit.MILLISECONDS) - doOnClose += { scheduledExecutor.shutdown() } - } - - return NodeInfoWebService(nodeInfoStorage, networkMapStorage, config) - } - - - private fun getDoormanService(config: DoormanConfig, database: CordaPersistence, signer: LocalSigner?, serverStatus: NetworkManagementServerStatus): RegistrationWebService { - logger.info("Starting Doorman server.") - val requestService = if (config.approveAll) { - logger.warn("Doorman server is in 'Approve All' mode, this will approve all incoming certificate signing requests.") - ApproveAllCertificateRequestStorage(PersistentCertificateRequestStorage(database)) - } else { - PersistentCertificateRequestStorage(database) - } - - val jiraConfig = config.jiraConfig - val requestProcessor = if (jiraConfig != null) { - val jiraWebAPI = AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(URI(jiraConfig.address), jiraConfig.username, jiraConfig.password) - val jiraClient = JiraClient(jiraWebAPI, jiraConfig.projectCode, jiraConfig.doneTransitionCode) - JiraCsrHandler(jiraClient, requestService, DefaultCsrHandler(requestService, signer)) - } else { - DefaultCsrHandler(requestService, signer) - } - - val scheduledExecutor = Executors.newScheduledThreadPool(1) - val approvalThread = Runnable { - try { - serverStatus.lastRequestCheckTime = Instant.now() - // Create tickets for requests which don't have one yet. - requestProcessor.createTickets() - // Process Jira approved tickets. - requestProcessor.processApprovedRequests() - } catch (e: Exception) { - // Log the error and carry on. - logger.error("Error encountered when approving request.", e) - } - } - scheduledExecutor.scheduleAtFixedRate(approvalThread, config.approveInterval, config.approveInterval, TimeUnit.MILLISECONDS) - doOnClose += { scheduledExecutor.shutdown() } - - return RegistrationWebService(requestProcessor) - } - - fun start(hostAndPort: NetworkHostAndPort, - database: CordaPersistence, - doormanSigner: LocalSigner? = null, - doormanServiceParameter: DoormanConfig?, // TODO Doorman config shouldn't be optional as the doorman is always required to run - startNetworkMap: NetworkMapStartParams? - ) { - val services = mutableListOf() - val serverStatus = NetworkManagementServerStatus() - - startNetworkMap?.let { services += getNetworkMapService(it.config, database, it.signer, it.updateNetworkParameters) } - doormanServiceParameter?.let { services += getDoormanService(it, database, doormanSigner, serverStatus) } - - require(services.isNotEmpty()) { "No service created, please provide at least one service config." } - - // TODO: use mbean to expose audit data? - services += MonitoringWebService(serverStatus) - - val webServer = NetworkManagementWebServer(hostAndPort, *services.toTypedArray()) - webServer.start() - - doOnClose += webServer::close - this.hostAndPort = webServer.hostAndPort - } -} - data class NetworkMapStartParams(val signer: LocalSigner?, val updateNetworkParameters: NetworkParameters?, val config: NetworkMapConfig) data class NetworkManagementServerStatus(var serverStartTime: Instant = Instant.now(), var lastRequestCheckTime: Instant? = null) @@ -181,9 +54,8 @@ fun generateRootKeyPair(rootStoreFile: Path, rootKeystorePass: String?, rootPriv val rootPrivateKeyPassword = rootPrivateKeyPass ?: readPassword("Root Private Key Password: ") if (rootStore.containsAlias(X509Utilities.CORDA_ROOT_CA)) { - val oldKey = loadOrCreateKeyStore(rootStoreFile, rootKeystorePassword).getCertificate(X509Utilities.CORDA_ROOT_CA).publicKey - println("Key ${X509Utilities.CORDA_ROOT_CA} already exists in keystore, process will now terminate.") - println(oldKey) + println("${X509Utilities.CORDA_ROOT_CA} already exists in keystore, process will now terminate.") + println(rootStore.getCertificate(X509Utilities.CORDA_ROOT_CA)) exitProcess(1) } @@ -203,7 +75,7 @@ fun generateRootKeyPair(rootStoreFile: Path, rootKeystorePass: String?, rootPriv println("Trust store for distribution to nodes created in $nodeTrustStore") println("Root CA keypair and certificate stored in ${rootStoreFile.toAbsolutePath()}.") - println(loadKeyStore(rootStoreFile, rootKeystorePassword).getCertificate(X509Utilities.CORDA_ROOT_CA).publicKey) + println(selfSignCert) } fun generateSigningKeyPairs(keystoreFile: Path, rootStoreFile: Path, rootKeystorePass: String?, rootPrivateKeyPass: String?, keystorePass: String?, caPrivateKeyPass: String?) { @@ -262,7 +134,7 @@ fun generateSigningKeyPairs(keystoreFile: Path, rootStoreFile: Path, rootKeystor } -private fun buildLocalSigners(parameters: NetworkManagementServerParameters): Pair? { +private fun processKeyStore(parameters: NetworkManagementServerParameters): Pair? { if (parameters.keystorePath == null) return null // Get password from console if not in config. @@ -270,19 +142,22 @@ private fun buildLocalSigners(parameters: NetworkManagementServerParameters): Pa val privateKeyPassword = parameters.caPrivateKeyPassword ?: readPassword("Private key password: ") val keyStore = loadOrCreateKeyStore(parameters.keystorePath, keyStorePassword) - val (doormanSigner, networkMapSigner) = listOf(DEFAULT_CSR_CERTIFICATE_NAME, DEFAULT_NETWORK_MAP_CERTIFICATE_NAME).map { - val keyPair = keyStore.getKeyPair(it, privateKeyPassword) - val certPath = keyStore.getCertificateChain(it).map { it as X509Certificate } - LocalSigner(keyPair, certPath.toTypedArray()) + val csrCertPathAndKey = keyStore.run { + CertPathAndKey( + keyStore.getCertificateChain(DEFAULT_CSR_CERTIFICATE_NAME).map { it as X509Certificate }, + keyStore.getSupportedKey(DEFAULT_CSR_CERTIFICATE_NAME, privateKeyPassword) + ) } - return Pair(doormanSigner, networkMapSigner) + val networkMapSigner = LocalSigner(keyStore.getCertificateAndKeyPair(DEFAULT_NETWORK_MAP_CERTIFICATE_NAME, privateKeyPassword)) + + return Pair(csrCertPathAndKey, networkMapSigner) } /** * This storage automatically approves all created requests. */ -private class ApproveAllCertificateRequestStorage(private val delegate: CertificationRequestStorage) : CertificationRequestStorage by delegate { +class ApproveAllCertificateRequestStorage(private val delegate: CertificationRequestStorage) : CertificationRequestStorage by delegate { override fun saveRequest(request: PKCS10CertificationRequest): String { val requestId = delegate.saveRequest(request) delegate.markRequestTicketCreated(requestId) @@ -311,9 +186,9 @@ fun main(args: Array) { initialiseSerialization() val database = configureDatabase(dataSourceProperties) // TODO: move signing to signing server. - val localSigners = buildLocalSigners(this) + val csrAndNetworkMap = processKeyStore(this) - if (localSigners != null) { + if (csrAndNetworkMap != null) { println("Starting network management services with local signing") } @@ -325,10 +200,10 @@ fun main(args: Array) { parseNetworkParametersFrom(it) } val networkMapStartParams = networkMapConfig?.let { - NetworkMapStartParams(localSigners?.second, networkParameters, it) + NetworkMapStartParams(csrAndNetworkMap?.second, networkParameters, it) } - networkManagementServer.start(NetworkHostAndPort(host, port), database, localSigners?.first, doormanConfig, networkMapStartParams) + networkManagementServer.start(NetworkHostAndPort(host, port), database, csrAndNetworkMap?.first, doormanConfig, networkMapStartParams) Runtime.getRuntime().addShutdownHook(thread(start = false) { networkManagementServer.close() diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/NetworkManagementServer.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/NetworkManagementServer.kt index e849c3b086..030d92ce36 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/NetworkManagementServer.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/NetworkManagementServer.kt @@ -1,58 +1,142 @@ package com.r3.corda.networkmanage.doorman +import com.atlassian.jira.rest.client.internal.async.AsynchronousJiraRestClientFactory +import com.r3.corda.networkmanage.common.persistence.PersistentCertificateRequestStorage +import com.r3.corda.networkmanage.common.persistence.PersistentNetworkMapStorage +import com.r3.corda.networkmanage.common.persistence.PersistentNodeInfoStorage +import com.r3.corda.networkmanage.common.signer.NetworkMapSigner +import com.r3.corda.networkmanage.common.utils.CertPathAndKey +import com.r3.corda.networkmanage.doorman.signer.DefaultCsrHandler +import com.r3.corda.networkmanage.doorman.signer.JiraCsrHandler +import com.r3.corda.networkmanage.doorman.signer.LocalSigner +import com.r3.corda.networkmanage.doorman.webservice.MonitoringWebService +import com.r3.corda.networkmanage.doorman.webservice.NetworkMapWebService +import com.r3.corda.networkmanage.doorman.webservice.RegistrationWebService import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.loggerFor -import org.eclipse.jetty.server.Server -import org.eclipse.jetty.server.ServerConnector -import org.eclipse.jetty.server.handler.HandlerCollection -import org.eclipse.jetty.servlet.ServletContextHandler -import org.eclipse.jetty.servlet.ServletHolder -import org.glassfish.jersey.server.ResourceConfig -import org.glassfish.jersey.servlet.ServletContainer +import net.corda.nodeapi.internal.network.NetworkParameters +import net.corda.nodeapi.internal.persistence.CordaPersistence import java.io.Closeable -import java.net.InetSocketAddress +import java.net.URI +import java.time.Instant +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit -/** - * NetworkManagementWebServer runs on Jetty server and provides service via http. - */ -class NetworkManagementWebServer(hostAndPort: NetworkHostAndPort, private vararg val webServices: Any) : Closeable { +class NetworkManagementServer : Closeable { companion object { - val logger = loggerFor() + private val logger = loggerFor() } - private val server: Server = Server(InetSocketAddress(hostAndPort.host, hostAndPort.port)).apply { - handler = HandlerCollection().apply { - addHandler(buildServletContextHandler()) - } - } - - val hostAndPort: NetworkHostAndPort - get() = server.connectors.mapNotNull { it as? ServerConnector } - .map { NetworkHostAndPort(it.host, it.localPort) } - .first() + private val closeActions = mutableListOf<() -> Unit>() + lateinit var hostAndPort: NetworkHostAndPort override fun close() { - logger.info("Shutting down network management web services...") - server.stop() - server.join() - } - - fun start() { - logger.info("Starting network management web services...") - server.start() - logger.info("Network management web services started on $hostAndPort with ${webServices.map { it.javaClass.simpleName }}") - println("Network management web services started on $hostAndPort with ${webServices.map { it.javaClass.simpleName }}") - } - - private fun buildServletContextHandler(): ServletContextHandler { - return ServletContextHandler().apply { - contextPath = "/" - val resourceConfig = ResourceConfig().apply { - // Add your API provider classes (annotated for JAX-RS) here - webServices.forEach { register(it) } + for (closeAction in closeActions) { + try { + closeAction() + } catch (e: Exception) { + logger.warn("Discregarding exception thrown during close", e) } - val jerseyServlet = ServletHolder(ServletContainer(resourceConfig)).apply { initOrder = 0 }// Initialise at server start - addServlet(jerseyServlet, "/*") } } + + private fun getNetworkMapService(config: NetworkMapConfig, database: CordaPersistence, signer: LocalSigner?, newNetworkParameters: NetworkParameters?): NetworkMapWebService { + val networkMapStorage = PersistentNetworkMapStorage(database) + val nodeInfoStorage = PersistentNodeInfoStorage(database) + val localNetworkMapSigner = signer?.let { NetworkMapSigner(networkMapStorage, it) } + + newNetworkParameters?.let { + val netParamsOfNetworkMap = networkMapStorage.getNetworkParametersOfNetworkMap() + if (netParamsOfNetworkMap == null) { + localNetworkMapSigner?.persistSignedNetworkParameters(it) ?: networkMapStorage.saveNetworkParameters(it, null) + } else { + throw UnsupportedOperationException("Network parameters already exist. Updating them is not supported yet.") + } + } + + val latestParameters = networkMapStorage.getLatestNetworkParameters() ?: + throw IllegalStateException("No network parameters were found. Please upload new network parameters before starting network map service") + logger.info("Starting network map service with network parameters: $latestParameters") + + if (localNetworkMapSigner != null) { + logger.info("Starting background worker for signing the network map using the local key store") + val scheduledExecutor = Executors.newScheduledThreadPool(1) + scheduledExecutor.scheduleAtFixedRate({ + try { + localNetworkMapSigner.signNetworkMap() + } catch (e: Exception) { + // Log the error and carry on. + logger.error("Unable to sign network map", e) + } + }, config.signInterval, config.signInterval, TimeUnit.MILLISECONDS) + closeActions += scheduledExecutor::shutdown + } + + return NetworkMapWebService(nodeInfoStorage, networkMapStorage, config) + } + + + private fun getDoormanService(config: DoormanConfig, + database: CordaPersistence, + csrCertPathAndKey: CertPathAndKey?, + serverStatus: NetworkManagementServerStatus): RegistrationWebService { + logger.info("Starting Doorman server.") + val requestService = if (config.approveAll) { + logger.warn("Doorman server is in 'Approve All' mode, this will approve all incoming certificate signing requests.") + ApproveAllCertificateRequestStorage(PersistentCertificateRequestStorage(database)) + } else { + PersistentCertificateRequestStorage(database) + } + + val jiraConfig = config.jiraConfig + val requestProcessor = if (jiraConfig != null) { + val jiraWebAPI = AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(URI(jiraConfig.address), jiraConfig.username, jiraConfig.password) + val jiraClient = JiraClient(jiraWebAPI, jiraConfig.projectCode, jiraConfig.doneTransitionCode) + JiraCsrHandler(jiraClient, requestService, DefaultCsrHandler(requestService, csrCertPathAndKey)) + } else { + DefaultCsrHandler(requestService, csrCertPathAndKey) + } + + val scheduledExecutor = Executors.newScheduledThreadPool(1) + val approvalThread = Runnable { + try { + serverStatus.lastRequestCheckTime = Instant.now() + // Create tickets for requests which don't have one yet. + requestProcessor.createTickets() + // Process Jira approved tickets. + requestProcessor.processApprovedRequests() + } catch (e: Exception) { + // Log the error and carry on. + logger.error("Error encountered when approving request.", e) + } + } + scheduledExecutor.scheduleAtFixedRate(approvalThread, config.approveInterval, config.approveInterval, TimeUnit.MILLISECONDS) + closeActions += scheduledExecutor::shutdown + + return RegistrationWebService(requestProcessor) + } + + fun start(hostAndPort: NetworkHostAndPort, + database: CordaPersistence, + csrCertPathAndKey: CertPathAndKey?, + doormanServiceParameter: DoormanConfig?, // TODO Doorman config shouldn't be optional as the doorman is always required to run + startNetworkMap: NetworkMapStartParams? + ) { + val services = mutableListOf() + val serverStatus = NetworkManagementServerStatus() + + startNetworkMap?.let { services += getNetworkMapService(it.config, database, it.signer, it.updateNetworkParameters) } + doormanServiceParameter?.let { services += getDoormanService(it, database, csrCertPathAndKey, serverStatus) } + + require(services.isNotEmpty()) { "No service created, please provide at least one service config." } + + // TODO: use mbean to expose audit data? + services += MonitoringWebService(serverStatus) + + val webServer = NetworkManagementWebServer(hostAndPort, *services.toTypedArray()) + webServer.start() + + closeActions += webServer::close + this.hostAndPort = webServer.hostAndPort + } } \ No newline at end of file diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/NetworkManagementWebServer.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/NetworkManagementWebServer.kt new file mode 100644 index 0000000000..fb99e68c44 --- /dev/null +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/NetworkManagementWebServer.kt @@ -0,0 +1,59 @@ +package com.r3.corda.networkmanage.doorman + +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.contextLogger +import net.corda.core.utilities.loggerFor +import org.eclipse.jetty.server.Server +import org.eclipse.jetty.server.ServerConnector +import org.eclipse.jetty.server.handler.HandlerCollection +import org.eclipse.jetty.servlet.ServletContextHandler +import org.eclipse.jetty.servlet.ServletHolder +import org.glassfish.jersey.server.ResourceConfig +import org.glassfish.jersey.servlet.ServletContainer +import java.io.Closeable +import java.net.InetSocketAddress + +/** + * NetworkManagementWebServer runs on Jetty server and provides service via http. + */ +class NetworkManagementWebServer(hostAndPort: NetworkHostAndPort, private vararg val webServices: Any) : Closeable { + companion object { + val logger = contextLogger() + } + + private val server: Server = Server(InetSocketAddress(hostAndPort.host, hostAndPort.port)).apply { + handler = HandlerCollection().apply { + addHandler(buildServletContextHandler()) + } + } + + val hostAndPort: NetworkHostAndPort + get() = server.connectors.mapNotNull { it as? ServerConnector } + .map { NetworkHostAndPort(it.host, it.localPort) } + .first() + + override fun close() { + logger.info("Shutting down network management web services...") + server.stop() + server.join() + } + + fun start() { + logger.info("Starting network management web services...") + server.start() + logger.info("Network management web services started on $hostAndPort with ${webServices.map { it.javaClass.simpleName }}") + println("Network management web services started on $hostAndPort with ${webServices.map { it.javaClass.simpleName }}") + } + + private fun buildServletContextHandler(): ServletContextHandler { + return ServletContextHandler().apply { + contextPath = "/" + val resourceConfig = ResourceConfig().apply { + // Add your API provider classes (annotated for JAX-RS) here + webServices.forEach { register(it) } + } + val jerseyServlet = ServletHolder(ServletContainer(resourceConfig)).apply { initOrder = 0 }// Initialise at server start + addServlet(jerseyServlet, "/*") + } + } +} \ No newline at end of file diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/CsrHandler.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/CsrHandler.kt index 2a6a9e5ee4..207a118c88 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/CsrHandler.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/CsrHandler.kt @@ -1,13 +1,20 @@ package com.r3.corda.networkmanage.doorman.signer import com.r3.corda.networkmanage.common.persistence.CertificateResponse -import com.r3.corda.networkmanage.common.persistence.CertificateSigningRequest import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage.Companion.DOORMAN_SIGNATURE import com.r3.corda.networkmanage.common.persistence.RequestStatus -import com.r3.corda.networkmanage.doorman.JiraClient -import net.corda.core.utilities.loggerFor +import com.r3.corda.networkmanage.common.utils.CertPathAndKey +import net.corda.nodeapi.internal.crypto.CertificateType +import net.corda.nodeapi.internal.crypto.X509CertificateFactory +import net.corda.nodeapi.internal.crypto.X509Utilities +import org.bouncycastle.asn1.x509.GeneralName +import org.bouncycastle.asn1.x509.GeneralSubtree +import org.bouncycastle.asn1.x509.NameConstraints import org.bouncycastle.pkcs.PKCS10CertificationRequest +import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest +import java.security.cert.CertPath +import javax.security.auth.x500.X500Principal interface CsrHandler { fun saveRequest(rawRequest: PKCS10CertificationRequest): String @@ -16,26 +23,21 @@ interface CsrHandler { fun getResponse(requestId: String): CertificateResponse } -class DefaultCsrHandler(private val storage: CertificationRequestStorage, private val signer: LocalSigner?) : CsrHandler { +class DefaultCsrHandler(private val storage: CertificationRequestStorage, + private val csrCertPathAndKey: CertPathAndKey?) : CsrHandler { + override fun processApprovedRequests() { - storage.getRequests(RequestStatus.APPROVED) - .forEach { processRequest(it.requestId, it.request) } + if (csrCertPathAndKey == null) return + storage.getRequests(RequestStatus.APPROVED).forEach { + val nodeCertPath = createSignedNodeCertificate(it.request, csrCertPathAndKey) + // Since Doorman is deployed in the auto-signing mode, we use DOORMAN_SIGNATURE as the signer. + storage.putCertificatePath(it.requestId, nodeCertPath, listOf(DOORMAN_SIGNATURE)) + } } override fun createTickets() {} - private fun processRequest(requestId: String, request: PKCS10CertificationRequest) { - if (signer != null) { - val certs = signer.createSignedClientCertificate(request) - // Since Doorman is deployed in the auto-signing mode (i.e. signer != null), - // we use DOORMAN_SIGNATURE as the signer. - storage.putCertificatePath(requestId, certs, listOf(DOORMAN_SIGNATURE)) - } - } - - override fun saveRequest(rawRequest: PKCS10CertificationRequest): String { - return storage.saveRequest(rawRequest) - } + override fun saveRequest(rawRequest: PKCS10CertificationRequest): String = storage.saveRequest(rawRequest) override fun getResponse(requestId: String): CertificateResponse { val response = storage.getRequest(requestId) @@ -45,63 +47,25 @@ class DefaultCsrHandler(private val storage: CertificationRequestStorage, privat RequestStatus.SIGNED -> CertificateResponse.Ready(response.certData?.certPath ?: throw IllegalArgumentException("Certificate should not be null.")) } } -} -class JiraCsrHandler(private val jiraClient: JiraClient, private val storage: CertificationRequestStorage, private val delegate: CsrHandler) : CsrHandler by delegate { - private companion object { - val log = loggerFor() - } - - override fun saveRequest(rawRequest: PKCS10CertificationRequest): String { - val requestId = delegate.saveRequest(rawRequest) - // Make sure request has been accepted. - try { - if (delegate.getResponse(requestId) !is CertificateResponse.Unauthorised) { - jiraClient.createRequestTicket(requestId, rawRequest) - storage.markRequestTicketCreated(requestId) - } - } catch (e: Exception) { - log.warn("There was an error while creating Jira tickets", e) - } finally { - return requestId - } - } - - override fun processApprovedRequests() { - val approvedRequest = jiraClient.getApprovedRequests() - approvedRequest.forEach { (id, approvedBy) -> storage.approveRequest(id, approvedBy) } - delegate.processApprovedRequests() - - val signedRequests = approvedRequest.mapNotNull { (id, _) -> - val request = storage.getRequest(id) - - if (request != null && request.status == RequestStatus.SIGNED) { - request.certData?.certPath?.let { certs -> id to certs } - } else { - null - } - }.toMap() - jiraClient.updateSignedRequests(signedRequests) - } - - /** - * Creates Jira tickets for all request in [RequestStatus.NEW] state. - * - * Usually requests are expected to move to the [RequestStatus.TICKET_CREATED] state immediately, - * they might be left in the [RequestStatus.NEW] state if Jira is down. - */ - override fun createTickets() { - try { - for (signingRequest in storage.getRequests(RequestStatus.NEW)) { - createTicket(signingRequest) - } - } catch (e: Exception) { - log.warn("There were errors while creating Jira tickets", e) - } - } - - private fun createTicket(signingRequest: CertificateSigningRequest) { - jiraClient.createRequestTicket(signingRequest.requestId, signingRequest.request) - storage.markRequestTicketCreated(signingRequest.requestId) + private fun createSignedNodeCertificate(certificationRequest: PKCS10CertificationRequest, + csrCertPathAndKey: CertPathAndKey): CertPath { + // 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 request = JcaPKCS10CertificationRequest(certificationRequest) + val nameConstraints = NameConstraints( + arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, request.subject))), + arrayOf()) + val nodeCaCert = X509Utilities.createCertificate( + CertificateType.NODE_CA, + csrCertPathAndKey.certPath[0], + csrCertPathAndKey.toKeyPair(), + X500Principal(request.subject.encoded), + request.publicKey, + nameConstraints = nameConstraints) + return X509CertificateFactory().generateCertPath(listOf(nodeCaCert) + csrCertPathAndKey.certPath) } } diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/JiraCsrHandler.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/JiraCsrHandler.kt new file mode 100644 index 0000000000..e7f9d51046 --- /dev/null +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/JiraCsrHandler.kt @@ -0,0 +1,69 @@ +package com.r3.corda.networkmanage.doorman.signer + +import com.r3.corda.networkmanage.common.persistence.CertificateResponse +import com.r3.corda.networkmanage.common.persistence.CertificateSigningRequest +import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage +import com.r3.corda.networkmanage.common.persistence.RequestStatus +import com.r3.corda.networkmanage.doorman.JiraClient +import net.corda.core.utilities.contextLogger +import net.corda.core.utilities.loggerFor +import org.bouncycastle.pkcs.PKCS10CertificationRequest + +class JiraCsrHandler(private val jiraClient: JiraClient, private val storage: CertificationRequestStorage, private val delegate: CsrHandler) : CsrHandler by delegate { + private companion object { + val log = contextLogger() + } + + override fun saveRequest(rawRequest: PKCS10CertificationRequest): String { + val requestId = delegate.saveRequest(rawRequest) + // Make sure request has been accepted. + try { + if (delegate.getResponse(requestId) !is CertificateResponse.Unauthorised) { + jiraClient.createRequestTicket(requestId, rawRequest) + storage.markRequestTicketCreated(requestId) + } + } catch (e: Exception) { + log.warn("There was an error while creating Jira tickets", e) + } finally { + return requestId + } + } + + override fun processApprovedRequests() { + val approvedRequest = jiraClient.getApprovedRequests() + approvedRequest.forEach { (id, approvedBy) -> storage.approveRequest(id, approvedBy) } + delegate.processApprovedRequests() + + val signedRequests = approvedRequest.mapNotNull { (id, _) -> + val request = storage.getRequest(id) + + if (request != null && request.status == RequestStatus.SIGNED) { + request.certData?.certPath?.let { certs -> id to certs } + } else { + null + } + }.toMap() + jiraClient.updateSignedRequests(signedRequests) + } + + /** + * Creates Jira tickets for all request in [RequestStatus.NEW] state. + * + * Usually requests are expected to move to the [RequestStatus.TICKET_CREATED] state immediately, + * they might be left in the [RequestStatus.NEW] state if Jira is down. + */ + override fun createTickets() { + try { + for (signingRequest in storage.getRequests(RequestStatus.NEW)) { + createTicket(signingRequest) + } + } catch (e: Exception) { + log.warn("There were errors while creating Jira tickets", e) + } + } + + private fun createTicket(signingRequest: CertificateSigningRequest) { + jiraClient.createRequestTicket(signingRequest.requestId, signingRequest.request) + storage.markRequestTicketCreated(signingRequest.requestId) + } +} diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/LocalSigner.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/LocalSigner.kt index 18343d7e44..aed84388c6 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/LocalSigner.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/LocalSigner.kt @@ -1,48 +1,20 @@ package com.r3.corda.networkmanage.doorman.signer import com.r3.corda.networkmanage.common.signer.Signer -import com.r3.corda.networkmanage.common.utils.buildCertPath import net.corda.core.crypto.Crypto import net.corda.core.internal.DigitalSignatureWithCert -import net.corda.nodeapi.internal.crypto.CertificateType -import net.corda.nodeapi.internal.crypto.X509Utilities -import org.bouncycastle.asn1.x509.GeneralName -import org.bouncycastle.asn1.x509.GeneralSubtree -import org.bouncycastle.asn1.x509.NameConstraints -import org.bouncycastle.pkcs.PKCS10CertificationRequest -import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest -import java.security.KeyPair -import java.security.cert.CertPath +import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair +import java.security.KeyStore +import java.security.PrivateKey import java.security.cert.X509Certificate -import javax.security.auth.x500.X500Principal /** - * The [LocalSigner] class signs [PKCS10CertificationRequest] using provided CA key pair and certificate path. - * This is intended to be used in testing environment where hardware signing module is not available. + * This local signer is intended to be used in testing environment where hardware signing module is not available. */ -//TODO Use a list instead of array -class LocalSigner(private val signingKeyPair: KeyPair, private val signingCertPath: Array) : Signer { - // TODO This doesn't belong in this class - fun createSignedClientCertificate(certificationRequest: PKCS10CertificationRequest): CertPath { - // 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 request = JcaPKCS10CertificationRequest(certificationRequest) - val nameConstraints = NameConstraints( - arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, request.subject))), - arrayOf()) - val nodeCaCert = X509Utilities.createCertificate( - CertificateType.NODE_CA, - signingCertPath[0], - signingKeyPair, - X500Principal(request.subject.encoded), - request.publicKey, - nameConstraints = nameConstraints) - return buildCertPath(nodeCaCert, *signingCertPath) - } +class LocalSigner(private val signingKey: PrivateKey, private val signingCert: X509Certificate) : Signer { + constructor(certAndKeyPair: CertificateAndKeyPair) : this(certAndKeyPair.keyPair.private, certAndKeyPair.certificate) override fun signBytes(data: ByteArray): DigitalSignatureWithCert { - return DigitalSignatureWithCert(signingCertPath[0], Crypto.doSign(signingKeyPair.private, data)) + return DigitalSignatureWithCert(signingCert, Crypto.doSign(signingKey, data)) } } diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/NodeInfoWebService.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/NetworkMapWebService.kt similarity index 91% rename from network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/NodeInfoWebService.kt rename to network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/NetworkMapWebService.kt index 57418b3d9c..93706599c5 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/NodeInfoWebService.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/NetworkMapWebService.kt @@ -7,9 +7,8 @@ import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage import com.r3.corda.networkmanage.common.persistence.NodeInfoStorage import com.r3.corda.networkmanage.common.persistence.NodeInfoWithSigned import com.r3.corda.networkmanage.common.utils.SignedNetworkMap -import com.r3.corda.networkmanage.common.utils.SignedNetworkParameters import com.r3.corda.networkmanage.doorman.NetworkMapConfig -import com.r3.corda.networkmanage.doorman.webservice.NodeInfoWebService.Companion.NETWORK_MAP_PATH +import com.r3.corda.networkmanage.doorman.webservice.NetworkMapWebService.Companion.NETWORK_MAP_PATH import net.corda.core.crypto.SecureHash import net.corda.core.node.NodeInfo import net.corda.core.serialization.deserialize @@ -31,9 +30,9 @@ import javax.ws.rs.core.Response.ok import javax.ws.rs.core.Response.status @Path(NETWORK_MAP_PATH) -class NodeInfoWebService(private val nodeInfoStorage: NodeInfoStorage, - private val networkMapStorage: NetworkMapStorage, - private val config: NetworkMapConfig) { +class NetworkMapWebService(private val nodeInfoStorage: NodeInfoStorage, + private val networkMapStorage: NetworkMapStorage, + private val config: NetworkMapConfig) { companion object { val log = contextLogger() @@ -42,7 +41,7 @@ class NodeInfoWebService(private val nodeInfoStorage: NodeInfoStorage, private val networkMapCache: LoadingCache> = CacheBuilder.newBuilder() .expireAfterWrite(config.cacheTimeout, TimeUnit.MILLISECONDS) - .build(CacheLoader.from { _ -> Pair(networkMapStorage.getCurrentNetworkMap(), networkMapStorage.getCurrentSignedNetworkParameters()?.verified()) }) + .build(CacheLoader.from { _ -> Pair(networkMapStorage.getCurrentNetworkMap(), networkMapStorage.getNetworkParametersOfNetworkMap()?.verified()) }) @POST @Path("publish") diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/Main.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/Main.kt index 4e25900f68..4ab7e0a7f7 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/Main.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/Main.kt @@ -1,7 +1,10 @@ package com.r3.corda.networkmanage.hsm +import com.google.common.util.concurrent.MoreExecutors +import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage import com.r3.corda.networkmanage.common.persistence.PersistentNetworkMapStorage import com.r3.corda.networkmanage.common.persistence.configureDatabase +import com.r3.corda.networkmanage.common.signer.NetworkMapSigner import com.r3.corda.networkmanage.common.utils.ShowHelpException import com.r3.corda.networkmanage.hsm.authentication.AuthMode import com.r3.corda.networkmanage.hsm.authentication.Authenticator @@ -16,10 +19,19 @@ import com.r3.corda.networkmanage.hsm.persistence.DBSignedCertificateRequestStor import com.r3.corda.networkmanage.hsm.signer.HsmCsrSigner import com.r3.corda.networkmanage.hsm.signer.HsmNetworkMapSigner import com.r3.corda.networkmanage.hsm.utils.mapCryptoServerException +import net.corda.core.utilities.minutes +import org.apache.logging.log4j.LogManager import org.bouncycastle.jce.provider.BouncyCastleProvider import java.security.Security +import java.time.Duration +import java.util.concurrent.Executors +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.TimeUnit.MILLISECONDS +import java.util.concurrent.TimeUnit.SECONDS import javax.crypto.Cipher +private val log = LogManager.getLogger("com.r3.corda.networkmanage.hsm.Main") + fun main(args: Array) { // Grabbed from https://stackoverflow.com/questions/7953567/checking-if-unlimited-cryptography-is-available if (Cipher.getMaxAllowedKeyLength("AES") < 256) { @@ -46,12 +58,15 @@ fun run(parameters: Parameters) { checkNotNull(dataSourceProperties) val database = configureDatabase(dataSourceProperties, databaseConfig) val csrStorage = DBSignedCertificateRequestStorage(database) - val networkMapStorage = PersistentNetworkMapStorage(database) - val hsmNetworkMapSigningThread = HsmNetworkMapSigner( - networkMapStorage, + val hsmSigner = HsmNetworkMapSigner( networkMapCertificateName, networkMapPrivateKeyPassword, - Authenticator(createProvider(), AuthMode.KEY_FILE, autoUsername, authKeyFilePath, authKeyFilePassword, signAuthThreshold)).start() + Authenticator(createProvider(), AuthMode.KEY_FILE, autoUsername, authKeyFilePath, authKeyFilePassword, signAuthThreshold)) + + val networkMapStorage = PersistentNetworkMapStorage(database) + val scheduler = Executors.newSingleThreadScheduledExecutor() + startNetworkingMapSigningPolling(networkMapStorage, hsmSigner, scheduler, 10.minutes) + val sign: (List) -> Unit = { val signer = HsmCsrSigner( csrStorage, @@ -62,6 +77,7 @@ fun run(parameters: Parameters) { Authenticator(createProvider(), authMode, autoUsername, authKeyFilePath, authKeyFilePassword, signAuthThreshold)) signer.sign(it) } + Menu().withExceptionHandler(::processError).addItem("1", "Generate root and intermediate certificates", { if (confirmedKeyGen()) { val generator = KeyCertificateGenerator( @@ -104,10 +120,25 @@ fun run(parameters: Parameters) { println("There is no approved and unsigned CSR") } }).showMenu() - hsmNetworkMapSigningThread.stop() + + MoreExecutors.shutdownAndAwaitTermination(scheduler, 30, SECONDS) } } +private fun startNetworkingMapSigningPolling(networkMapStorage: NetworkMapStorage, + signer: HsmNetworkMapSigner, + executor: ScheduledExecutorService, + signingPeriod: Duration) { + val networkMapSigner = NetworkMapSigner(networkMapStorage, signer) + executor.scheduleAtFixedRate({ + try { + networkMapSigner.signNetworkMap() + } catch (e: Exception) { + log.warn("Exception thrown while signing network map", e) + } + }, signingPeriod.toMillis(), signingPeriod.toMillis(), MILLISECONDS) +} + private fun processError(exception: Exception) { val processed = mapCryptoServerException(exception) System.err.println("An error occurred: ${processed.message}") diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/signer/HsmNetworkMapSigner.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/signer/HsmNetworkMapSigner.kt index 322233220f..aa0c4e6e17 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/signer/HsmNetworkMapSigner.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/signer/HsmNetworkMapSigner.kt @@ -1,78 +1,39 @@ package com.r3.corda.networkmanage.hsm.signer -import com.google.common.util.concurrent.MoreExecutors -import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage -import com.r3.corda.networkmanage.common.signer.NetworkMapSigner import com.r3.corda.networkmanage.common.signer.Signer import com.r3.corda.networkmanage.hsm.authentication.Authenticator import com.r3.corda.networkmanage.hsm.utils.X509Utilities import com.r3.corda.networkmanage.hsm.utils.X509Utilities.getAndInitializeKeyStore import com.r3.corda.networkmanage.hsm.utils.X509Utilities.verify import net.corda.core.internal.DigitalSignatureWithCert -import net.corda.core.utilities.loggerFor -import net.corda.core.utilities.minutes +import net.corda.nodeapi.internal.crypto.getX509Certificate import java.security.PrivateKey import java.security.Signature -import java.security.cert.X509Certificate -import java.time.Duration -import java.util.concurrent.Executors -import java.util.concurrent.ScheduledExecutorService -import java.util.concurrent.TimeUnit /** - * Encapsulates logic for periodic network map signing execution. - * It uses HSM as the signing entity with keys and certificates specified at the construction time. + * Signer which connects to a HSM using the given [authenticator] to sign bytes. */ // TODO Rename this to HsmSigner -class HsmNetworkMapSigner(networkMapStorage: NetworkMapStorage, - private val caCertificateKeyName: String, - private val caPrivateKeyPass: String, - private val authenticator: Authenticator, - private val signingPeriod: Duration = DEFAULT_SIGNING_PERIOD_MS) : Signer { - - companion object { - val log = loggerFor() - val DEFAULT_SIGNING_PERIOD_MS = 10.minutes - - private val TERMINATION_TIMEOUT_SEC = 2L - } - - private val networkMapSigner = NetworkMapSigner(networkMapStorage, this) - private lateinit var scheduledExecutor: ScheduledExecutorService - - // TODO This doesn't belong in this class - fun start(): HsmNetworkMapSigner { - val signingPeriodMillis = signingPeriod.toMillis() - scheduledExecutor = Executors.newSingleThreadScheduledExecutor() - scheduledExecutor.scheduleAtFixedRate({ - try { - networkMapSigner.signNetworkMap() - } catch (exception: Exception) { - log.warn("Exception thrown while signing network map", exception) - } - }, signingPeriodMillis, signingPeriodMillis, TimeUnit.MILLISECONDS) - return this - } - - fun stop() { - MoreExecutors.shutdownAndAwaitTermination(scheduledExecutor, TERMINATION_TIMEOUT_SEC, TimeUnit.SECONDS) - } - +class HsmNetworkMapSigner(private val certificateKeyName: String, + private val privateKeyPassword: String, + private val authenticator: Authenticator) : Signer { /** * Signs given data using [CryptoServerJCE.CryptoServerProvider], which connects to the underlying HSM. */ override fun signBytes(data: ByteArray): DigitalSignatureWithCert { return authenticator.connectAndAuthenticate { provider, _ -> val keyStore = getAndInitializeKeyStore(provider) - val caCertificateChain = keyStore.getCertificateChain(caCertificateKeyName) - val caKey = keyStore.getKey(caCertificateKeyName, caPrivateKeyPass.toCharArray()) as PrivateKey + val certificate = keyStore.getX509Certificate(certificateKeyName) + // Don't worry this is not a real private key but a pointer to one that resides in the HSM. It only works + // when used with the given provider. + val key = keyStore.getKey(certificateKeyName, privateKeyPassword.toCharArray()) as PrivateKey val signature = Signature.getInstance(X509Utilities.SIGNATURE_ALGORITHM, provider).run { - initSign(caKey) + initSign(key) update(data) sign() } - verify(data, signature, caCertificateChain[0].publicKey) - DigitalSignatureWithCert(caCertificateChain[0] as X509Certificate, signature) + verify(data, signature, certificate.publicKey) + DigitalSignatureWithCert(certificate, signature) } } } diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNetworkMapStorageTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNetworkMapStorageTest.kt index af75cf07b7..77d041da43 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNetworkMapStorageTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNetworkMapStorageTest.kt @@ -65,7 +65,7 @@ class PersistentNetworkMapStorageTest : TestBase() { // then val persistedSignedNetworkMap = networkMapStorage.getCurrentNetworkMap() - val persistedSignedParameters = networkMapStorage.getCurrentSignedNetworkParameters() + val persistedSignedParameters = networkMapStorage.getNetworkParametersOfNetworkMap() assertEquals(networkParameters, persistedSignedParameters?.verifiedNetworkMapCert(rootCaCert)) assertEquals(parametersSignature, persistedSignedParameters?.sig) @@ -84,13 +84,13 @@ class PersistentNetworkMapStorageTest : TestBase() { networkMapStorage.saveNetworkParameters(params2, null) // when - val latest = networkMapStorage.getLatestUnsignedNetworkParameters() + val latest = networkMapStorage.getLatestNetworkParameters()?.minimumPlatformVersion // then - assertEquals(2, latest.minimumPlatformVersion) + assertEquals(2, latest) } @Test - fun `getCurrentNetworkParameters returns current network map parameters`() { + fun `getNetworkParametersOfNetworkMap returns current network map parameters`() { // given // Create network parameters val testParameters1 = testNetworkParameters(emptyList()) @@ -107,7 +107,7 @@ class PersistentNetworkMapStorageTest : TestBase() { networkMapStorage.saveNetworkParameters(testParameters2, testParameters2.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate).sig) // when - val result = networkMapStorage.getCurrentSignedNetworkParameters()?.verifiedNetworkMapCert(rootCaCert) + val result = networkMapStorage.getNetworkParametersOfNetworkMap()?.verifiedNetworkMapCert(rootCaCert) // then assertEquals(1, result?.minimumPlatformVersion) diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/signer/NetworkMapSignerTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/signer/NetworkMapSignerTest.kt index dc29826d1b..25d9f7e7f5 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/signer/NetworkMapSignerTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/signer/NetworkMapSignerTest.kt @@ -51,8 +51,8 @@ class NetworkMapSignerTest : TestBase() { val signedNetworkMap = networkMap.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate) whenever(networkMapStorage.getCurrentNetworkMap()).thenReturn(signedNetworkMap) whenever(networkMapStorage.getNodeInfoHashes(any())).thenReturn(signedNodeInfoHashes) - whenever(networkMapStorage.getLatestUnsignedNetworkParameters()).thenReturn(latestNetworkParameters) - whenever(networkMapStorage.getCurrentSignedNetworkParameters()).thenReturn(currentParameters.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)) + whenever(networkMapStorage.getLatestNetworkParameters()).thenReturn(latestNetworkParameters) + whenever(networkMapStorage.getNetworkParametersOfNetworkMap()).thenReturn(currentParameters.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)) whenever(signer.signBytes(any())).then { DigitalSignatureWithCert(networkMapCa.certificate, Crypto.doSign(networkMapCa.keyPair.private, it.arguments[0] as ByteArray)) } @@ -67,8 +67,8 @@ class NetworkMapSignerTest : TestBase() { // then // Verify networkMapStorage calls verify(networkMapStorage).getNodeInfoHashes(any()) - verify(networkMapStorage).getLatestUnsignedNetworkParameters() - verify(networkMapStorage).getCurrentSignedNetworkParameters() + verify(networkMapStorage).getLatestNetworkParameters() + verify(networkMapStorage).getNetworkParametersOfNetworkMap() argumentCaptor().apply { verify(networkMapStorage).saveNetworkMap(capture()) val capturedNetworkMap = firstValue.verifiedNetworkMapCert(rootCaCert) @@ -87,8 +87,8 @@ class NetworkMapSignerTest : TestBase() { val signedNetworkMap = networkMap.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate) whenever(networkMapStorage.getCurrentNetworkMap()).thenReturn(signedNetworkMap) whenever(networkMapStorage.getNodeInfoHashes(any())).thenReturn(emptyList()) - whenever(networkMapStorage.getLatestUnsignedNetworkParameters()).thenReturn(networkParameters) - whenever(networkMapStorage.getCurrentSignedNetworkParameters()).thenReturn(networkParameters.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)) + whenever(networkMapStorage.getLatestNetworkParameters()).thenReturn(networkParameters) + whenever(networkMapStorage.getNetworkParametersOfNetworkMap()).thenReturn(networkParameters.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)) // when networkMapSigner.signNetworkMap() @@ -104,7 +104,7 @@ class NetworkMapSignerTest : TestBase() { val networkParameters = testNetworkParameters(emptyList()) whenever(networkMapStorage.getCurrentNetworkMap()).thenReturn(null) whenever(networkMapStorage.getNodeInfoHashes(any())).thenReturn(emptyList()) - whenever(networkMapStorage.getLatestUnsignedNetworkParameters()).thenReturn(networkParameters) + whenever(networkMapStorage.getLatestNetworkParameters()).thenReturn(networkParameters) whenever(signer.signBytes(any())).then { DigitalSignatureWithCert(networkMapCa.certificate, Crypto.doSign(networkMapCa.keyPair.private, it.arguments[0] as ByteArray)) } @@ -118,7 +118,7 @@ class NetworkMapSignerTest : TestBase() { // then // Verify networkMapStorage calls verify(networkMapStorage).getNodeInfoHashes(any()) - verify(networkMapStorage).getLatestUnsignedNetworkParameters() + verify(networkMapStorage).getLatestNetworkParameters() argumentCaptor().apply { verify(networkMapStorage).saveNetworkMap(capture()) val networkMap = firstValue.verifiedNetworkMapCert(rootCaCert) diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/DefaultRequestProcessorTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/DefaultRequestProcessorTest.kt deleted file mode 100644 index b0091a4cf6..0000000000 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/DefaultRequestProcessorTest.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.r3.corda.networkmanage.doorman - -import com.nhaarman.mockito_kotlin.any -import com.nhaarman.mockito_kotlin.mock -import com.nhaarman.mockito_kotlin.times -import com.nhaarman.mockito_kotlin.verify -import com.r3.corda.networkmanage.TestBase -import com.r3.corda.networkmanage.common.persistence.CertificateResponse -import com.r3.corda.networkmanage.common.persistence.CertificateStatus -import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage -import com.r3.corda.networkmanage.common.persistence.RequestStatus -import com.r3.corda.networkmanage.common.utils.buildCertPath -import com.r3.corda.networkmanage.doorman.signer.DefaultCsrHandler -import com.r3.corda.networkmanage.doorman.signer.LocalSigner -import net.corda.core.crypto.Crypto -import net.corda.nodeapi.internal.crypto.X509Utilities -import org.junit.Test -import javax.security.auth.x500.X500Principal -import kotlin.test.assertEquals - -class DefaultRequestProcessorTest : TestBase() { - @Test - fun `get response`() { - val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val cert = X509Utilities.createSelfSignedCACertificate(X500Principal("O=Test,L=London,C=GB"), keyPair) - - val requestStorage: CertificationRequestStorage = mock { - on { getRequest("New") }.thenReturn(certificateSigningRequest()) - on { getRequest("Signed") }.thenReturn(certificateSigningRequest(status = RequestStatus.SIGNED, certData = certificateData("", CertificateStatus.VALID, buildCertPath(cert)))) - on { getRequest("Rejected") }.thenReturn(certificateSigningRequest(status = RequestStatus.REJECTED, remark = "Random reason")) - } - val signer: LocalSigner = mock() - val requestProcessor = DefaultCsrHandler(requestStorage, signer) - - assertEquals(CertificateResponse.NotReady, requestProcessor.getResponse("random")) - assertEquals(CertificateResponse.NotReady, requestProcessor.getResponse("New")) - assertEquals(CertificateResponse.Ready(buildCertPath(cert)), requestProcessor.getResponse("Signed")) - assertEquals(CertificateResponse.Unauthorised("Random reason"), requestProcessor.getResponse("Rejected")) - } - - @Test - fun `process request`() { - val (request1, request2, request3) = (1..3).map { - X509Utilities.createCertificateSigningRequest(X500Principal("O=Test1,L=London,C=GB"), "my@email.com", Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)) - } - - val requestStorage: CertificationRequestStorage = mock { - on { getRequests(RequestStatus.APPROVED) }.thenReturn(listOf( - certificateSigningRequest(requestId = "1", request = request1, status = RequestStatus.APPROVED), - certificateSigningRequest(requestId = "2", request = request2, status = RequestStatus.APPROVED), - certificateSigningRequest(requestId = "3", request = request3, status = RequestStatus.APPROVED) - )) - } - val signer: LocalSigner = mock() - val requestProcessor = DefaultCsrHandler(requestStorage, signer) - - requestProcessor.processApprovedRequests() - - verify(signer, times(3)).createSignedClientCertificate(any()) - verify(requestStorage, times(1)).getRequests(any()) - } -} diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/DefaultCsrHandlerTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/DefaultCsrHandlerTest.kt new file mode 100644 index 0000000000..e518801bec --- /dev/null +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/DefaultCsrHandlerTest.kt @@ -0,0 +1,85 @@ +package com.r3.corda.networkmanage.doorman.signer + +import com.nhaarman.mockito_kotlin.* +import com.r3.corda.networkmanage.TestBase +import com.r3.corda.networkmanage.common.persistence.CertificateResponse +import com.r3.corda.networkmanage.common.persistence.CertificateStatus +import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage +import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage.Companion.DOORMAN_SIGNATURE +import com.r3.corda.networkmanage.common.persistence.RequestStatus +import com.r3.corda.networkmanage.common.utils.CertPathAndKey +import com.r3.corda.networkmanage.common.utils.buildCertPath +import net.corda.core.crypto.Crypto +import net.corda.core.internal.CertRole +import net.corda.nodeapi.internal.crypto.X509Utilities +import net.corda.testing.internal.createDevIntermediateCaCertPath +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import java.security.cert.CertPath +import java.security.cert.X509Certificate +import javax.security.auth.x500.X500Principal +import kotlin.test.assertEquals + +class DefaultCsrHandlerTest : TestBase() { + @Test + fun getResponse() { + val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val cert = X509Utilities.createSelfSignedCACertificate(X500Principal("O=Test,L=London,C=GB"), keyPair) + + val requestStorage: CertificationRequestStorage = mock { + on { getRequest("New") }.thenReturn(certificateSigningRequest()) + on { getRequest("Signed") }.thenReturn(certificateSigningRequest(status = RequestStatus.SIGNED, certData = certificateData("", CertificateStatus.VALID, buildCertPath(cert)))) + on { getRequest("Rejected") }.thenReturn(certificateSigningRequest(status = RequestStatus.REJECTED, remark = "Random reason")) + } + val requestProcessor = DefaultCsrHandler(requestStorage, null) + + assertEquals(CertificateResponse.NotReady, requestProcessor.getResponse("random")) + assertEquals(CertificateResponse.NotReady, requestProcessor.getResponse("New")) + assertEquals(CertificateResponse.Ready(buildCertPath(cert)), requestProcessor.getResponse("Signed")) + assertEquals(CertificateResponse.Unauthorised("Random reason"), requestProcessor.getResponse("Rejected")) + } + + @Test + fun processApprovedRequests() { + val requests = (1..3).map { + X509Utilities.createCertificateSigningRequest( + X500Principal("O=Test$it,L=London,C=GB"), + "my@email.com", + Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)) + } + + val requestStorage: CertificationRequestStorage = mock { + on { getRequests(RequestStatus.APPROVED) }.thenReturn(listOf( + certificateSigningRequest(requestId = "1", request = requests[0], status = RequestStatus.APPROVED), + certificateSigningRequest(requestId = "2", request = requests[1], status = RequestStatus.APPROVED) + )) + on { getRequests(RequestStatus.REJECTED) }.thenReturn(listOf( + certificateSigningRequest(requestId = "3", request = requests[2], status = RequestStatus.REJECTED) + )) + } + + val (rootCa, csrCa) = createDevIntermediateCaCertPath() + val csrCertPathAndKey = CertPathAndKey(listOf(csrCa.certificate, rootCa.certificate), csrCa.keyPair.private) + val requestProcessor = DefaultCsrHandler(requestStorage, csrCertPathAndKey) + + requestProcessor.processApprovedRequests() + + val certPathCapture = argumentCaptor() + + // Verify only the approved requests are taken + verify(requestStorage, times(1)).getRequests(RequestStatus.APPROVED) + verify(requestStorage, times(1)).putCertificatePath(eq("1"), certPathCapture.capture(), eq(listOf(DOORMAN_SIGNATURE))) + verify(requestStorage, times(1)).putCertificatePath(eq("2"), certPathCapture.capture(), eq(listOf(DOORMAN_SIGNATURE))) + + // Then make sure the generated node cert paths are correct + certPathCapture.allValues.forEachIndexed { index, certPath -> + X509Utilities.validateCertificateChain(rootCa.certificate, *certPath.certificates.toTypedArray()) + assertThat(certPath.certificates).hasSize(3).element(1).isEqualTo(csrCa.certificate) + (certPath.certificates[0] as X509Certificate).apply { + assertThat(CertRole.extract(this)).isEqualTo(CertRole.NODE_CA) + assertThat(publicKey).isEqualTo(Crypto.toSupportedPublicKey(requests[index].subjectPublicKeyInfo)) + assertThat(subjectX500Principal).isEqualTo(X500Principal("O=Test${index + 1},L=London,C=GB")) + } + } + } +} diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/CsrHandlerTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/JiraCsrHandlerTest.kt similarity index 90% rename from network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/CsrHandlerTest.kt rename to network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/JiraCsrHandlerTest.kt index 42314f4b38..19498bb744 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/CsrHandlerTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/JiraCsrHandlerTest.kt @@ -14,25 +14,26 @@ import org.junit.Rule import org.junit.Test import org.mockito.Mock import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule import java.security.cert.CertPath class JiraCsrHandlerTest { @Rule @JvmField - val mockitoRule = MockitoJUnit.rule() + val mockitoRule: MockitoRule = MockitoJUnit.rule() @Mock - lateinit var jiraClient: JiraClient + private lateinit var jiraClient: JiraClient @Mock - lateinit var certificationRequestStorage: CertificationRequestStorage + private lateinit var certificationRequestStorage: CertificationRequestStorage @Mock - lateinit var defaultCsrHandler: DefaultCsrHandler + private lateinit var defaultCsrHandler: DefaultCsrHandler @Mock - var certPath: CertPath = mock() + private val certPath: CertPath = mock() private lateinit var jiraCsrHandler: JiraCsrHandler private val requestId = "id" diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/NodeInfoWebServiceTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/webservice/NetworkMapWebServiceTest.kt similarity index 86% rename from network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/NodeInfoWebServiceTest.kt rename to network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/webservice/NetworkMapWebServiceTest.kt index 037daf4892..430d298a47 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/NodeInfoWebServiceTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/webservice/NetworkMapWebServiceTest.kt @@ -1,4 +1,4 @@ -package com.r3.corda.networkmanage.doorman +package com.r3.corda.networkmanage.doorman.webservice import com.nhaarman.mockito_kotlin.mock import com.nhaarman.mockito_kotlin.times @@ -7,7 +7,8 @@ import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage import com.r3.corda.networkmanage.common.persistence.NodeInfoStorage import com.r3.corda.networkmanage.common.utils.SignedNetworkMap import com.r3.corda.networkmanage.common.utils.SignedNetworkParameters -import com.r3.corda.networkmanage.doorman.webservice.NodeInfoWebService +import com.r3.corda.networkmanage.doorman.NetworkManagementWebServer +import com.r3.corda.networkmanage.doorman.NetworkMapConfig import net.corda.core.crypto.SecureHash.Companion.randomSHA256 import net.corda.core.identity.CordaX500Name import net.corda.core.internal.checkOkResponse @@ -36,7 +37,7 @@ import java.security.cert.X509Certificate import javax.ws.rs.core.MediaType import kotlin.test.assertEquals -class NodeInfoWebServiceTest { +class NetworkMapWebServiceTest { @Rule @JvmField val testSerialization = SerializationEnvironmentRule(true) @@ -56,12 +57,12 @@ class NodeInfoWebServiceTest { @Test fun `submit nodeInfo`() { val networkMapStorage: NetworkMapStorage = mock { - on { getCurrentSignedNetworkParameters() }.thenReturn(testNetworkParameters(emptyList()).signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)) + on { getNetworkParametersOfNetworkMap() }.thenReturn(testNetworkParameters(emptyList()).signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)) } // Create node info. val (_, signedNodeInfo) = createNodeInfoAndSigned(CordaX500Name("Test", "London", "GB")) - NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(mock(), networkMapStorage, testNetworkMapConfig)).use { + NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(mock(), networkMapStorage, testNetworkMapConfig)).use { it.start() val nodeInfoAndSignature = signedNodeInfo.serialize().bytes // Post node info and signature to doorman, this should pass without any exception. @@ -72,12 +73,12 @@ class NodeInfoWebServiceTest { @Test fun `submit old nodeInfo`() { val networkMapStorage: NetworkMapStorage = mock { - on { getCurrentSignedNetworkParameters() }.thenReturn(testNetworkParameters(emptyList(), minimumPlatformVersion = 2).signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)) + on { getNetworkParametersOfNetworkMap() }.thenReturn(testNetworkParameters(emptyList(), minimumPlatformVersion = 2).signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)) } // Create node info. val (_, signedNodeInfo) = createNodeInfoAndSigned(CordaX500Name("Test", "London", "GB"), platformVersion = 1) - NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(mock(), networkMapStorage, testNetworkMapConfig)).use { + NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(mock(), networkMapStorage, testNetworkMapConfig)).use { it.start() val nodeInfoAndSignature = signedNodeInfo.serialize().bytes assertThatThrownBy { it.doPost("publish", nodeInfoAndSignature) } @@ -88,12 +89,12 @@ class NodeInfoWebServiceTest { @Test fun `submit nodeInfo when no network parameters`() { val networkMapStorage: NetworkMapStorage = mock { - on { getCurrentSignedNetworkParameters() }.thenReturn(null) + on { getNetworkParametersOfNetworkMap() }.thenReturn(null) } // Create node info. val (_, signedNodeInfo) = createNodeInfoAndSigned(CordaX500Name("Test", "London", "GB"), platformVersion = 1) - NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(mock(), networkMapStorage, testNetworkMapConfig)).use { + NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(mock(), networkMapStorage, testNetworkMapConfig)).use { it.start() val nodeInfoAndSignature = signedNodeInfo.serialize().bytes assertThatThrownBy { it.doPost("publish", nodeInfoAndSignature) } @@ -110,7 +111,7 @@ class NodeInfoWebServiceTest { on { getCurrentNetworkMap() }.thenReturn(signedNetworkMap) } - NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(mock(), networkMapStorage, testNetworkMapConfig)).use { + NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(mock(), networkMapStorage, testNetworkMapConfig)).use { it.start() val signedNetworkMapResponse = it.doGet("") verify(networkMapStorage, times(1)).getCurrentNetworkMap() @@ -127,7 +128,7 @@ class NodeInfoWebServiceTest { on { getNodeInfo(nodeInfoHash) }.thenReturn(signedNodeInfo) } - NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(nodeInfoStorage, mock(), testNetworkMapConfig)).use { + NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(nodeInfoStorage, mock(), testNetworkMapConfig)).use { it.start() val nodeInfoResponse = it.doGet("node-info/$nodeInfoHash") verify(nodeInfoStorage, times(1)).getNodeInfo(nodeInfoHash) @@ -149,7 +150,7 @@ class NodeInfoWebServiceTest { on { getSignedNetworkParameters(networkParametersHash) }.thenReturn(signedNetworkParameters) } - NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(mock(), networkMapStorage, testNetworkMapConfig)).use { + NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(mock(), networkMapStorage, testNetworkMapConfig)).use { it.start() val netParamsResponse = it.doGet("network-parameters/$networkParametersHash") verify(networkMapStorage, times(1)).getSignedNetworkParameters(networkParametersHash) diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/RegistrationWebServiceTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/webservice/RegistrationWebServiceTest.kt similarity index 98% rename from network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/RegistrationWebServiceTest.kt rename to network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/webservice/RegistrationWebServiceTest.kt index dc1b0d1949..d9821f38fc 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/RegistrationWebServiceTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/webservice/RegistrationWebServiceTest.kt @@ -1,11 +1,11 @@ -package com.r3.corda.networkmanage.doorman +package com.r3.corda.networkmanage.doorman.webservice import com.nhaarman.mockito_kotlin.* import com.r3.corda.networkmanage.TestBase import com.r3.corda.networkmanage.common.persistence.CertificateResponse import com.r3.corda.networkmanage.common.utils.buildCertPath +import com.r3.corda.networkmanage.doorman.NetworkManagementWebServer import com.r3.corda.networkmanage.doorman.signer.CsrHandler -import com.r3.corda.networkmanage.doorman.webservice.RegistrationWebService import net.corda.core.crypto.Crypto import net.corda.core.crypto.SecureHash import net.corda.core.identity.CordaX500Name