Various cleanups of the network-management code (#545)

This commit is contained in:
Shams Asari 2018-03-13 10:58:04 +00:00 committed by GitHub
parent 98f554c08d
commit e22e7acd67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 282 additions and 306 deletions

View File

@ -17,7 +17,6 @@ import com.r3.corda.networkmanage.doorman.signer.LocalSigner
import net.corda.cordform.CordformNode
import net.corda.core.crypto.random63BitValue
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.concurrent.transpose
import net.corda.core.internal.div
import net.corda.core.internal.exists
import net.corda.core.internal.list
@ -94,7 +93,9 @@ class NodeRegistrationTest : IntegrationTest() {
@Test
fun `register nodes with doorman and then they transact with each other`() {
// Start the server without the network parameters since we don't have them yet
// Start the server without the network parameters config which won't start the network map. Just the doorman
// registration process will start up, allowing us to register the notaries which will then be used in the network
// parameters.
server = startNetworkManagementServer(networkParameters = null)
val compatibilityZone = CompatibilityZoneParams(
URL("http://$serverAddress"),
@ -120,14 +121,12 @@ class NodeRegistrationTest : IntegrationTest() {
val (alice, notary) = listOf(
startNode(providedName = aliceName),
defaultNotaryNode
).transpose().getOrThrow()
alice as NodeHandleInternal
notary as NodeHandleInternal
).map { it.getOrThrow() as NodeHandleInternal }
alice.onlySeesFromNetworkMap(alice, notary)
notary.onlySeesFromNetworkMap(alice, notary)
val genevieve = startNode(providedName = genevieveName).getOrThrow()
genevieve as NodeHandleInternal
val genevieve = startNode(providedName = genevieveName).getOrThrow() as NodeHandleInternal
// Wait for the nodes to poll again
Thread.sleep(timeoutMillis * 2)
@ -161,7 +160,7 @@ class NodeRegistrationTest : IntegrationTest() {
networkParameters?.let {
NetworkMapStartParams(
LocalSigner(networkMapCa),
networkParameters,
it,
NetworkMapConfig(cacheTimeout = timeoutMillis, signInterval = timeoutMillis)
)
}

View File

@ -0,0 +1,16 @@
package com.r3.corda.networkmanage.common.persistence
import com.r3.corda.networkmanage.common.persistence.CertificateSigningRequestStorage
import org.bouncycastle.pkcs.PKCS10CertificationRequest
/**
* This storage automatically approves all created requests.
*/
class ApproveAllCertificateSigningRequestStorage(private val delegate: CertificateSigningRequestStorage) : CertificateSigningRequestStorage by delegate {
override fun saveRequest(request: PKCS10CertificationRequest): String {
val requestId = delegate.saveRequest(request)
delegate.markRequestTicketCreated(requestId)
approveRequest(requestId, CertificateSigningRequestStorage.DOORMAN_SIGNATURE)
return requestId
}
}

View File

@ -12,6 +12,7 @@ package com.r3.corda.networkmanage.common.persistence
import net.corda.core.crypto.SecureHash
import net.corda.core.node.NodeInfo
import net.corda.nodeapi.internal.NodeInfoAndSigned
import net.corda.nodeapi.internal.SignedNodeInfo
import java.security.cert.CertPath
@ -32,9 +33,9 @@ interface NodeInfoStorage {
fun getNodeInfo(nodeInfoHash: SecureHash): SignedNodeInfo?
/**
* The [nodeInfo] is keyed by the public key, old node info with the same public key will be replaced by the new node info.
* @param signedNodeInfo signed node info data to be stored
* The [nodeInfoAndSigned] is keyed by the public key, old node info with the same public key will be replaced by the new node info.
* @param nodeInfoAndSigned signed node info data to be stored
* @return hash for the newly created node info entry
*/
fun putNodeInfo(signedNodeInfo: NodeInfoWithSigned): SecureHash
fun putNodeInfo(nodeInfoAndSigned: NodeInfoAndSigned): SecureHash
}

View File

@ -1,18 +0,0 @@
/*
* R3 Proprietary and Confidential
*
* Copyright (c) 2018 R3 Limited. All rights reserved.
*
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
*
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
*/
package com.r3.corda.networkmanage.common.persistence
import net.corda.core.node.NodeInfo
import net.corda.nodeapi.internal.SignedNodeInfo
class NodeInfoWithSigned(val signedNodeInfo: SignedNodeInfo) {
val nodeInfo: NodeInfo = signedNodeInfo.verified()
}

View File

@ -16,7 +16,9 @@ import com.r3.corda.networkmanage.common.utils.buildCertPath
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.sha256
import net.corda.core.internal.CertRole
import net.corda.core.internal.CertRole.NODE_CA
import net.corda.core.serialization.serialize
import net.corda.nodeapi.internal.NodeInfoAndSigned
import net.corda.nodeapi.internal.SignedNodeInfo
import net.corda.nodeapi.internal.crypto.x509Certificates
import net.corda.nodeapi.internal.persistence.CordaPersistence
@ -27,17 +29,18 @@ import java.security.cert.CertPath
* Database implementation of the [NetworkMapStorage] interface
*/
class PersistentNodeInfoStorage(private val database: CordaPersistence) : NodeInfoStorage {
override fun putNodeInfo(nodeInfoWithSigned: NodeInfoWithSigned): SecureHash {
val nodeInfo = nodeInfoWithSigned.nodeInfo
val signedNodeInfo = nodeInfoWithSigned.signedNodeInfo
val nodeCaCert = nodeInfo.legalIdentitiesAndCerts[0].certPath.x509Certificates.find { CertRole.extract(it) == CertRole.NODE_CA }
override fun putNodeInfo(nodeInfoAndSigned: NodeInfoAndSigned): SecureHash {
val (nodeInfo, signedNodeInfo) = nodeInfoAndSigned
val nodeCaCert = nodeInfo.legalIdentitiesAndCerts[0].certPath.x509Certificates.find { CertRole.extract(it) == NODE_CA }
nodeCaCert ?: throw IllegalArgumentException("Missing Node CA")
return database.transaction {
// TODO Move these checks out of data access layer
val request = nodeCaCert?.let {
getSignedRequestByPublicHash(it.publicKey.encoded.sha256(), this)
val request = requireNotNull(getSignedRequestByPublicHash(nodeCaCert.publicKey.encoded.sha256(), this)) {
"Node-info not registered with us"
}
request.certificateData?.certificateStatus.let {
require(it == CertificateStatus.VALID) { "Certificate is no longer valid: $it" }
}
request ?: throw IllegalArgumentException("Unknown node info, this public key is not registered with the network management service.")
require(request.certificateData!!.certificateStatus == CertificateStatus.VALID) { "Certificate is no longer valid" }
/*
* Delete any previous [NodeInfoEntity] instance for this CSR

View File

@ -22,7 +22,7 @@ import java.nio.file.Path
import java.nio.file.Paths
import java.util.*
data class NetworkManagementServerParameters(// TODO: Move local signing to signing server.
data class NetworkManagementServerConfig( // TODO: Move local signing to signing server.
val host: String,
val port: Int,
val dataSourceProperties: Properties,
@ -62,11 +62,11 @@ data class NetworkManagementServerParameters(// TODO: Move local signing to sign
data class DoormanConfig(val approveAll: Boolean = false,
val jira: JiraConfig? = null,
val approveInterval: Long = NetworkManagementServerParameters.DEFAULT_APPROVE_INTERVAL.toMillis())
val approveInterval: Long = NetworkManagementServerConfig.DEFAULT_APPROVE_INTERVAL.toMillis())
data class NetworkMapConfig(val cacheTimeout: Long,
// TODO: Move signing to signing server.
val signInterval: Long = NetworkManagementServerParameters.DEFAULT_SIGN_INTERVAL.toMillis())
val signInterval: Long = NetworkManagementServerConfig.DEFAULT_SIGN_INTERVAL.toMillis())
enum class Mode {
// TODO CA_KEYGEN now also generates the network map cert, so it should be renamed.
@ -85,7 +85,7 @@ data class JiraConfig(
/**
* Parses the doorman command line options.
*/
fun parseParameters(vararg args: String): NetworkManagementServerParameters {
fun parseParameters(vararg args: String): NetworkManagementServerConfig {
val argConfig = args.toConfigWithOptions {
accepts("config-file", "The path to the config file")
.withRequiredArg()
@ -112,7 +112,7 @@ fun parseParameters(vararg args: String): NetworkManagementServerParameters {
val config = argConfig.withFallback(ConfigFactory.parseFile(configFile.toFile(), ConfigParseOptions.defaults().setAllowMissing(true)))
.resolve()
.parseAs<NetworkManagementServerParameters>(false)
.parseAs<NetworkManagementServerConfig>(false)
// Make sure trust store password is only specified in root keygen mode.
if (config.mode != Mode.ROOT_KEYGEN) {

View File

@ -10,8 +10,7 @@
package com.r3.corda.networkmanage.doorman
import com.r3.corda.networkmanage.common.persistence.CertificateSigningRequestStorage
import com.r3.corda.networkmanage.common.persistence.CertificateSigningRequestStorage.Companion.DOORMAN_SIGNATURE
import com.jcabi.manifests.Manifests
import com.r3.corda.networkmanage.common.persistence.configureDatabase
import com.r3.corda.networkmanage.common.utils.*
import com.r3.corda.networkmanage.doorman.signer.LocalSigner
@ -19,94 +18,92 @@ import net.corda.core.node.NetworkParameters
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.nodeapi.internal.crypto.X509KeyStore
import net.corda.nodeapi.internal.crypto.X509Utilities
import org.bouncycastle.pkcs.PKCS10CertificationRequest
import java.time.Instant
import kotlin.concurrent.thread
import com.jcabi.manifests.Manifests
import kotlin.system.exitProcess
fun main(args: Array<String>) {
if (Manifests.exists("Doorman-Version")) {
println("Version: ${Manifests.read("Doorman-Version")}")
}
val parameters = try {
parseParameters(*args)
} catch (e: ShowHelpException) {
e.errorMessage?.let(::println)
e.parser.printHelpOn(System.out)
exitProcess(0)
}
// TODO Use the logger for this and elsewhere in this file.
println("Running in ${parameters.mode} mode")
when (parameters.mode) {
Mode.ROOT_KEYGEN -> parameters.rootKeyGenMode()
Mode.CA_KEYGEN -> parameters.caKeyGenMode()
Mode.DOORMAN -> parameters.doormanMode()
}
}
data class NetworkMapStartParams(val signer: LocalSigner?, val updateNetworkParameters: NetworkParameters?, val config: NetworkMapConfig)
data class NetworkManagementServerStatus(var serverStartTime: Instant = Instant.now(), var lastRequestCheckTime: Instant? = null)
private fun processKeyStore(parameters: NetworkManagementServerParameters): Pair<CertPathAndKey, LocalSigner>? {
if (parameters.keystorePath == null) return null
private fun processKeyStore(config: NetworkManagementServerConfig): Pair<CertPathAndKey, LocalSigner>? {
if (config.keystorePath == null) return null
// Get password from console if not in config.
val keyStorePassword = parameters.keystorePassword ?: readPassword("Key store password: ")
val privateKeyPassword = parameters.caPrivateKeyPassword ?: readPassword("Private key password: ")
val keyStore = X509KeyStore.fromFile(parameters.keystorePath, keyStorePassword)
val keyStorePassword = config.keystorePassword ?: readPassword("Key store password: ")
val privateKeyPassword = config.caPrivateKeyPassword ?: readPassword("Private key password: ")
val keyStore = X509KeyStore.fromFile(config.keystorePath, keyStorePassword)
val csrCertPathAndKey = keyStore.getCertPathAndKey(X509Utilities.CORDA_INTERMEDIATE_CA, privateKeyPassword)
val networkMapSigner = LocalSigner(keyStore.getCertificateAndKeyPair(CORDA_NETWORK_MAP, privateKeyPassword))
return Pair(csrCertPathAndKey, networkMapSigner)
}
/**
* This storage automatically approves all created requests.
*/
class ApproveAllCertificateRequestStorage(private val delegate: CertificateSigningRequestStorage) : CertificateSigningRequestStorage by delegate {
override fun saveRequest(request: PKCS10CertificationRequest): String {
val requestId = delegate.saveRequest(request)
delegate.markRequestTicketCreated(requestId)
approveRequest(requestId, DOORMAN_SIGNATURE)
return requestId
}
private fun NetworkManagementServerConfig.rootKeyGenMode() {
generateRootKeyPair(
rootStorePath ?: throw IllegalArgumentException("The 'rootStorePath' parameter must be specified when generating keys!"),
rootKeystorePassword,
rootPrivateKeyPassword,
trustStorePassword
)
}
private fun logDoormanVersion() {
if (Manifests.exists("Doorman-Version")) {
println("Doorman Version: ${Manifests.read("Doorman-Version")}")
}
private fun NetworkManagementServerConfig.caKeyGenMode() {
generateSigningKeyPairs(
keystorePath ?: throw IllegalArgumentException("The 'keystorePath' parameter must be specified when generating keys!"),
rootStorePath ?: throw IllegalArgumentException("The 'rootStorePath' parameter must be specified when generating keys!"),
rootKeystorePassword,
rootPrivateKeyPassword,
keystorePassword,
caPrivateKeyPassword
)
}
fun main(args: Array<String>) {
try {
parseParameters(*args).run {
println("Starting in $mode mode")
logDoormanVersion()
when (mode) {
Mode.ROOT_KEYGEN -> generateRootKeyPair(
rootStorePath ?: throw IllegalArgumentException("The 'rootStorePath' parameter must be specified when generating keys!"),
rootKeystorePassword,
rootPrivateKeyPassword,
trustStorePassword)
Mode.CA_KEYGEN -> generateSigningKeyPairs(
keystorePath ?: throw IllegalArgumentException("The 'keystorePath' parameter must be specified when generating keys!"),
rootStorePath ?: throw IllegalArgumentException("The 'rootStorePath' parameter must be specified when generating keys!"),
rootKeystorePassword,
rootPrivateKeyPassword,
keystorePassword,
caPrivateKeyPassword)
Mode.DOORMAN -> {
initialiseSerialization()
val persistence = configureDatabase(dataSourceProperties, database)
// TODO: move signing to signing server.
val csrAndNetworkMap = processKeyStore(this)
private fun NetworkManagementServerConfig.doormanMode() {
initialiseSerialization()
val persistence = configureDatabase(dataSourceProperties, database)
// TODO: move signing to signing server.
val csrAndNetworkMap = processKeyStore(this)
if (csrAndNetworkMap != null) {
println("Starting network management services with local signing")
}
val networkManagementServer = NetworkManagementServer()
val networkParameters = updateNetworkParameters?.let {
// TODO This check shouldn't be needed. Fix up the config design.
requireNotNull(networkMap) { "'networkMapConfig' config is required for applying network parameters" }
println("Parsing network parameters from '${it.toAbsolutePath()}'...")
parseNetworkParametersFrom(it)
}
val networkMapStartParams = networkMap?.let {
NetworkMapStartParams(csrAndNetworkMap?.second, networkParameters, it)
}
networkManagementServer.start(NetworkHostAndPort(host, port), persistence, csrAndNetworkMap?.first, doorman, networkMapStartParams)
Runtime.getRuntime().addShutdownHook(thread(start = false) {
networkManagementServer.close()
})
}
}
}
} catch (e: ShowHelpException) {
e.errorMessage?.let(::println)
e.parser.printHelpOn(System.out)
if (csrAndNetworkMap != null) {
println("Starting network management services with local signing")
}
val networkManagementServer = NetworkManagementServer()
val networkParameters = updateNetworkParameters?.let {
// TODO This check shouldn't be needed. Fix up the config design.
requireNotNull(networkMap) { "'networkMap' config is required for applying network parameters" }
println("Parsing network parameters from '${it.toAbsolutePath()}'...")
parseNetworkParametersConfig(it).toNetworkParameters(modifiedTime = Instant.now(), epoch = 1)
}
val networkMapStartParams = networkMap?.let {
NetworkMapStartParams(csrAndNetworkMap?.second, networkParameters, it)
}
networkManagementServer.start(NetworkHostAndPort(host, port), persistence, csrAndNetworkMap?.first, doorman, networkMapStartParams)
Runtime.getRuntime().addShutdownHook(thread(start = false) {
networkManagementServer.close()
})
}

View File

@ -11,6 +11,7 @@
package com.r3.corda.networkmanage.doorman
import com.atlassian.jira.rest.client.internal.async.AsynchronousJiraRestClientFactory
import com.r3.corda.networkmanage.common.persistence.ApproveAllCertificateSigningRequestStorage
import com.r3.corda.networkmanage.common.persistence.PersistentCertificateSigningRequestStorage
import com.r3.corda.networkmanage.common.persistence.PersistentNetworkMapStorage
import com.r3.corda.networkmanage.common.persistence.PersistentNodeInfoStorage
@ -24,7 +25,7 @@ import com.r3.corda.networkmanage.doorman.webservice.NetworkMapWebService
import com.r3.corda.networkmanage.doorman.webservice.RegistrationWebService
import net.corda.core.node.NetworkParameters
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.loggerFor
import net.corda.core.utilities.contextLogger
import net.corda.nodeapi.internal.persistence.CordaPersistence
import java.io.Closeable
import java.net.URI
@ -35,7 +36,7 @@ import java.util.concurrent.TimeUnit
class NetworkManagementServer : Closeable {
companion object {
private val logger = loggerFor<NetworkManagementServer>()
private val logger = contextLogger()
}
private val closeActions = mutableListOf<() -> Unit>()
@ -95,7 +96,7 @@ class NetworkManagementServer : Closeable {
val requestService = if (config.approveAll) {
require(config.jira == null) { "Jira configuration cannot be specified when the approveAll parameter is set to true." }
logger.warn("Doorman server is in 'Approve All' mode, this will approve all incoming certificate signing requests.")
ApproveAllCertificateRequestStorage(PersistentCertificateSigningRequestStorage(database))
ApproveAllCertificateSigningRequestStorage(PersistentCertificateSigningRequestStorage(database))
} else {
PersistentCertificateSigningRequestStorage(database)
}

View File

@ -0,0 +1,68 @@
/*
* R3 Proprietary and Confidential
*
* Copyright (c) 2018 R3 Limited. All rights reserved.
*
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
*
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
*/
package com.r3.corda.networkmanage.doorman
import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigParseOptions
import net.corda.core.internal.exists
import net.corda.core.internal.readObject
import net.corda.core.node.NetworkParameters
import net.corda.core.node.NotaryInfo
import net.corda.nodeapi.internal.SignedNodeInfo
import net.corda.nodeapi.internal.config.parseAs
import java.nio.file.Path
import java.time.Instant
/**
* Data class representing a [NotaryInfo] which can be easily parsed by a typesafe [ConfigFactory].
* @property notaryNodeInfoFile path to the node info file of the notary node.
* @property validating whether the notary is validating
*/
data class NotaryConfig(private val notaryNodeInfoFile: Path,
private val validating: Boolean) {
fun toNotaryInfo(): NotaryInfo {
val nodeInfo = notaryNodeInfoFile.readObject<SignedNodeInfo>().verified()
// It is always the last identity (in the list of identities) that corresponds to the notary identity.
// In case of a single notary, the list has only one element. In case of distributed notaries the list has
// two items and the second one corresponds to the notary identity.
return NotaryInfo(nodeInfo.legalIdentities.last(), validating)
}
}
/**
* Data class containing the fields from [NetworkParameters] which can be read at start-up time from doorman.
* It is a proper subset of [NetworkParameters] except for the [notaries] field which is replaced by a list of
* [NotaryConfig] which is parsable.
*
* This is public only because [parseAs] needs to be able to call its constructor.
*/
data class NetworkParametersConfig(val minimumPlatformVersion: Int,
val notaries: List<NotaryConfig>,
val maxMessageSize: Int,
val maxTransactionSize: Int) {
fun toNetworkParameters(modifiedTime: Instant, epoch: Int): NetworkParameters {
return NetworkParameters(
minimumPlatformVersion,
notaries.map { it.toNotaryInfo() },
maxMessageSize,
maxTransactionSize,
modifiedTime,
epoch,
// TODO: Tudor, Michal - pass the actual network parameters where we figure out how
emptyMap()
)
}
}
fun parseNetworkParametersConfig(configFile: Path): NetworkParametersConfig {
check(configFile.exists()) { "File $configFile does not exist" }
return ConfigFactory.parseFile(configFile.toFile(), ConfigParseOptions.defaults()).parseAs()
}

View File

@ -1,84 +0,0 @@
/*
* R3 Proprietary and Confidential
*
* Copyright (c) 2018 R3 Limited. All rights reserved.
*
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
*
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
*/
package com.r3.corda.networkmanage.doorman
import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigParseOptions
import net.corda.core.internal.exists
import net.corda.core.internal.readAll
import net.corda.core.node.NetworkParameters
import net.corda.core.node.NotaryInfo
import net.corda.core.serialization.deserialize
import net.corda.nodeapi.internal.SignedNodeInfo
import net.corda.nodeapi.internal.config.parseAs
import java.nio.file.Path
import java.time.Instant
/**
* Initial value for [NetworkParameters.epoch].
*/
private const val DEFAULT_EPOCH = 1
/**
* Data class representing a [NotaryInfo] which can be easily parsed by a typesafe [ConfigFactory].
* @property notaryNodeInfoFile path to the node info file of the notary node.
* @property validating whether the notary is validating
*/
internal data class NotaryConfiguration(private val notaryNodeInfoFile: Path,
private val validating: Boolean) {
fun toNotaryInfo(): NotaryInfo {
val nodeInfo = notaryNodeInfoFile.readAll().deserialize<SignedNodeInfo>().verified()
// It is always the last identity (in the list of identities) that corresponds to the notary identity.
// In case of a single notary, the list has only one element. In case of distributed notaries the list has
// two items and the second one corresponds to the notary identity.
return NotaryInfo(nodeInfo.legalIdentities.last(), validating)
}
}
/**
* data class containing the fields from [NetworkParameters] which can be read at start-up time from doorman.
* It is a proper subset of [NetworkParameters] except for the [notaries] field which is replaced by a list of
* [NotaryConfiguration] which is parsable.
*
* This is public only because [parseAs] needs to be able to call its constructor.
*/
internal data class NetworkParametersConfiguration(val minimumPlatformVersion: Int,
val notaries: List<NotaryConfiguration>,
val maxMessageSize: Int,
val maxTransactionSize: Int)
/**
* Parses a file and returns a [NetworkParameters] instance.
*
* @return a [NetworkParameters] with values read from [configFile] except:
* an epoch of [DEFAULT_EPOCH] and
* a modifiedTime initialized with [Instant.now].
*/
fun parseNetworkParametersFrom(configFile: Path, epoch: Int = DEFAULT_EPOCH): NetworkParameters {
return parseNetworkParameters(parseNetworkParametersConfigurationFrom(configFile), epoch)
}
internal fun parseNetworkParametersConfigurationFrom(configFile: Path): NetworkParametersConfiguration {
check(configFile.exists()) { "File $configFile does not exist" }
return ConfigFactory.parseFile(configFile.toFile(), ConfigParseOptions.defaults())
.parseAs(NetworkParametersConfiguration::class)
}
internal fun parseNetworkParameters(configuration: NetworkParametersConfiguration, epoch: Int = DEFAULT_EPOCH): NetworkParameters {
return NetworkParameters(configuration.minimumPlatformVersion,
configuration.notaries.map { it.toNotaryInfo() },
configuration.maxMessageSize,
configuration.maxTransactionSize,
Instant.now(),
epoch,
// TODO: Tudor, Michal - pass the actual network parameters where we figure out how
emptyMap())
}

View File

@ -15,7 +15,6 @@ import com.google.common.cache.CacheLoader
import com.google.common.cache.LoadingCache
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.doorman.NetworkMapConfig
import com.r3.corda.networkmanage.doorman.webservice.NetworkMapWebService.Companion.NETWORK_MAP_PATH
@ -25,7 +24,9 @@ import net.corda.core.node.NodeInfo
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug
import net.corda.core.utilities.trace
import net.corda.nodeapi.internal.NodeInfoAndSigned
import net.corda.nodeapi.internal.SignedNodeInfo
import java.io.InputStream
import java.security.InvalidKeyException
@ -52,32 +53,32 @@ class NetworkMapWebService(private val nodeInfoStorage: NodeInfoStorage,
private val networkMapCache: LoadingCache<Boolean, Pair<SignedNetworkMap?, NetworkParameters?>> = CacheBuilder.newBuilder()
.expireAfterWrite(config.cacheTimeout, TimeUnit.MILLISECONDS)
.build(CacheLoader.from { _ -> Pair(networkMapStorage.getCurrentNetworkMap(), networkMapStorage.getNetworkParametersOfNetworkMap()?.verified()) })
.build(CacheLoader.from { _ ->
Pair(networkMapStorage.getCurrentNetworkMap(), networkMapStorage.getNetworkParametersOfNetworkMap()?.verified()) }
)
@POST
@Path("publish")
@Consumes(MediaType.APPLICATION_OCTET_STREAM)
fun registerNode(input: InputStream): Response {
val signedNodeInfo = input.readBytes().deserialize<SignedNodeInfo>()
var nodeInfo: NodeInfo? = null
return try {
// Store the NodeInfo
val nodeInfoWithSignature = NodeInfoWithSigned(signedNodeInfo)
logger.trace { "Processing 'publish' request from '${nodeInfoWithSignature.nodeInfo.legalIdentities.first().name.organisation}'" }
verifyNodeInfo(nodeInfoWithSignature.nodeInfo)
nodeInfoStorage.putNodeInfo(nodeInfoWithSignature)
logger.trace { "Stored 'publish' request from '${nodeInfoWithSignature.nodeInfo.legalIdentities.first().name.organisation}'" }
val nodeInfoAndSigned = NodeInfoAndSigned(signedNodeInfo)
nodeInfo = nodeInfoAndSigned.nodeInfo
logger.debug { "Publishing node-info: $nodeInfo" }
verifyNodeInfo(nodeInfo)
nodeInfoStorage.putNodeInfo(nodeInfoAndSigned)
ok()
} catch (e: Exception) {
// Catch exceptions thrown by signature verification.
logger.warn("Unable to process node-info: $nodeInfo", e)
when (e) {
is NetworkMapNotInitialisedException -> status(Response.Status.SERVICE_UNAVAILABLE).entity(e.message)
is InvalidPlatformVersionException -> status(Response.Status.BAD_REQUEST).entity(e.message)
is IllegalArgumentException, is InvalidKeyException, is SignatureException -> status(Response.Status.UNAUTHORIZED).entity(e.message)
// Rethrow e if its not one of the expected exception, the server will return http 500 internal error.
else -> {
logger.error("Unexpected error encountered while processing request.", e)
throw e
}
is InvalidKeyException, is SignatureException -> status(Response.Status.UNAUTHORIZED).entity(e.message)
// Rethrow e if its not one of the expected exception, the server will return http 500 internal error.
else -> throw e
}
}.build()
}
@ -107,20 +108,14 @@ class NetworkMapWebService(private val nodeInfoStorage: NodeInfoStorage,
@Path("my-ip")
fun myIp(@Context request: HttpServletRequest): Response {
val ip = request.getHeader("X-Forwarded-For")?.split(",")?.first() ?: "${request.remoteHost}:${request.remotePort}"
logger.trace { "Precessed ip request from client, IP: '$ip'" }
logger.trace { "Processed IP request from client, IP: '$ip'" }
return ok(ip).build()
}
private fun verifyNodeInfo(nodeInfo: NodeInfo) {
val minimumPlatformVersion = networkMapCache.get(true).second?.minimumPlatformVersion
if (minimumPlatformVersion == null) {
logger.error("Error processing request from node '${nodeInfo.legalIdentities.first().name.organisation}' : Network parameters have not been initialised")
logger.trace { "$nodeInfo" }
throw NetworkMapNotInitialisedException("Network parameters have not been initialised")
}
?: throw NetworkMapNotInitialisedException("Network parameters have not been initialised")
if (nodeInfo.platformVersion < minimumPlatformVersion) {
logger.error("Error processing request from node '${nodeInfo.legalIdentities.first().name.organisation}' : Minimum platform version is $minimumPlatformVersion")
logger.trace { "$nodeInfo" }
throw InvalidPlatformVersionException("Minimum platform version is $minimumPlatformVersion")
}
}

View File

@ -0,0 +1,76 @@
/*
* R3 Proprietary and Confidential
*
* Copyright (c) 2018 R3 Limited. All rights reserved.
*
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
*
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
*/
package com.r3.corda.networkmanage
import com.google.common.jimfs.Jimfs
import com.r3.corda.networkmanage.doorman.NetworkParametersConfig
import com.r3.corda.networkmanage.doorman.NotaryConfig
import net.corda.core.internal.copyTo
import net.corda.core.node.NotaryInfo
import net.corda.core.serialization.serialize
import net.corda.nodeapi.internal.SignedNodeInfo
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.internal.createNodeInfoAndSigned
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Rule
import org.junit.Test
import java.nio.file.Path
import java.time.Instant
import java.util.*
class NetworkParametersConfigTest {
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule()
private val fs = Jimfs.newFileSystem()
@After
fun cleanUp() {
fs.close()
}
@Test
fun toNetworkParameters() {
val (aliceNodeInfo, aliceSignedNodeInfo) = createNodeInfoAndSigned(ALICE_NAME)
val (bobNodeInfo, bobSignedNodeInfo) = createNodeInfoAndSigned(BOB_NAME)
val networkParametersConfig = NetworkParametersConfig(
notaries = listOf(
NotaryConfig(aliceSignedNodeInfo.writeToFile(), true),
NotaryConfig(bobSignedNodeInfo.writeToFile(), false)
),
maxMessageSize = 100,
maxTransactionSize = 100,
minimumPlatformVersion = 3
)
val modifiedTime = Instant.now()
val networkParameters = networkParametersConfig.toNetworkParameters(modifiedTime = modifiedTime, epoch = 2)
assertThat(networkParameters.modifiedTime).isEqualTo(modifiedTime)
assertThat(networkParameters.epoch).isEqualTo(2)
assertThat(networkParameters.notaries).containsExactly(
NotaryInfo(aliceNodeInfo.legalIdentities[0], true),
NotaryInfo(bobNodeInfo.legalIdentities[0], false)
)
assertThat(networkParameters.maxMessageSize).isEqualTo(100)
assertThat(networkParameters.maxTransactionSize).isEqualTo(100)
assertThat(networkParameters.minimumPlatformVersion).isEqualTo(3)
}
private fun SignedNodeInfo.writeToFile(): Path {
val path = fs.getPath(UUID.randomUUID().toString())
serialize().open().copyTo(path)
return path
}
}

View File

@ -1,79 +0,0 @@
/*
* R3 Proprietary and Confidential
*
* Copyright (c) 2018 R3 Limited. All rights reserved.
*
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
*
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
*/
package com.r3.corda.networkmanage
import com.r3.corda.networkmanage.doorman.NetworkParametersConfiguration
import com.r3.corda.networkmanage.doorman.NotaryConfiguration
import com.r3.corda.networkmanage.doorman.parseNetworkParameters
import com.r3.corda.networkmanage.doorman.parseNetworkParametersFrom
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.copyTo
import net.corda.core.internal.deleteIfExists
import net.corda.core.serialization.serialize
import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.internal.createNodeInfoAndSigned
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.nio.file.Path
import java.nio.file.Paths
import java.time.Instant
class NetworkParametersConfigurationTest {
@Rule
@JvmField
val tempFolder = TemporaryFolder()
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule()
private fun generateNetworkParametersConfiguration() = NetworkParametersConfiguration(
notaries = listOf(
NotaryConfiguration(generateNodeInfoFile("Test1"), true),
NotaryConfiguration(generateNodeInfoFile("Test2"), false)
),
maxMessageSize = 100,
maxTransactionSize = 100,
minimumPlatformVersion = 1
)
private fun generateNodeInfoFile(organisation: String): Path {
val (_, signedNodeInfo) = createNodeInfoAndSigned(CordaX500Name(organisation, "Madrid", "ES"))
val path = tempFolder.newFile().toPath()
path.deleteIfExists()
signedNodeInfo.serialize().open().copyTo(path)
return path
}
@Test
fun `reads an existing file`() {
val networkParameters = parseNetworkParameters(generateNetworkParametersConfiguration())
assertThat(networkParameters.minimumPlatformVersion).isEqualTo(1)
val notaries = networkParameters.notaries
assertThat(notaries).hasSize(2)
assertThat(notaries[0].validating).isTrue()
assertThat(notaries[1].validating).isFalse()
assertThat(networkParameters.maxMessageSize).isEqualTo(100)
assertThat(networkParameters.maxTransactionSize).isEqualTo(100)
assertThat(networkParameters.epoch).isEqualTo(1)
}
@Test
fun `throws on a non-existing file`() {
assertThatThrownBy {
parseNetworkParametersFrom(Paths.get("notHere"))
}.isInstanceOf(IllegalStateException::class.java)
}
}

View File

@ -17,6 +17,7 @@ import net.corda.core.crypto.SecureHash
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.CertRole
import net.corda.core.serialization.serialize
import net.corda.nodeapi.internal.NodeInfoAndSigned
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
import net.corda.nodeapi.internal.crypto.CertificateType
import net.corda.nodeapi.internal.crypto.X509Utilities
@ -114,7 +115,7 @@ class PersistentNodeInfoStorageTest : TestBase() {
// Create node info.
val (node1, key) = createValidSignedNodeInfo("Test", requestStorage)
val nodeInfo2 = node1.nodeInfo.copy(serial = 2)
val node2 = NodeInfoWithSigned(nodeInfo2.signWith(listOf(key)))
val node2 = NodeInfoAndSigned(nodeInfo2.signWith(listOf(key)))
val nodeInfo1Hash = nodeInfoStorage.putNodeInfo(node1)
assertEquals(node1.nodeInfo, nodeInfoStorage.getNodeInfo(nodeInfo1Hash)?.verified())
@ -137,12 +138,12 @@ class PersistentNodeInfoStorageTest : TestBase() {
// then
val persistedSignedNodeInfo = nodeInfoStorage.getNodeInfo(nodeInfoHash)
assertThat(persistedSignedNodeInfo?.signatures).isEqualTo(nodeInfoWithSigned.signedNodeInfo.signatures)
assertThat(persistedSignedNodeInfo?.signatures).isEqualTo(nodeInfoWithSigned.signed.signatures)
}
}
internal fun createValidSignedNodeInfo(organisation: String,
storage: CertificateSigningRequestStorage): Pair<NodeInfoWithSigned, PrivateKey> {
storage: CertificateSigningRequestStorage): Pair<NodeInfoAndSigned, PrivateKey> {
val (csr, nodeKeyPair) = createRequest(organisation, certRole = CertRole.NODE_CA)
val requestId = storage.saveRequest(csr)
storage.markRequestTicketCreated(requestId)
@ -151,5 +152,5 @@ internal fun createValidSignedNodeInfo(organisation: String,
val (identity, key) = nodeInfoBuilder.addIdentity(CordaX500Name.build(X500Principal(csr.subject.encoded)), nodeKeyPair)
storage.putCertificatePath(requestId, identity.certPath, "Test")
val (_, signedNodeInfo) = nodeInfoBuilder.buildWithSigned(1)
return Pair(NodeInfoWithSigned(signedNodeInfo), key)
return Pair(NodeInfoAndSigned(signedNodeInfo), key)
}