ENT-1626 Validating cert path on node info submission (#650)

* Validating cert path on node info submission

* Addressing review comments

* Refactoring user exceptions

* Changing response message.
This commit is contained in:
Michal Kit 2018-04-05 14:21:54 +01:00 committed by GitHub
parent 2543bb356e
commit 84398362ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 198 additions and 84 deletions

View File

@ -141,12 +141,11 @@ class NetworkParametersUpdateTest : IntegrationTest() {
} }
private fun startServer(startNetworkMap: Boolean = true): NetworkManagementServer { private fun startServer(startNetworkMap: Boolean = true): NetworkManagementServer {
val server = NetworkManagementServer(makeTestDataSourceProperties(dbName), DatabaseConfig(runMigration = true)) val doormanConfig = DoormanConfig(approveAll = true, jira = null, approveInterval = timeoutMillis)
val server = NetworkManagementServer(makeTestDataSourceProperties(dbName), DatabaseConfig(runMigration = true), doormanConfig, null)
server.start( server.start(
serverAddress, serverAddress,
CertPathAndKey(listOf(doormanCa.certificate, rootCaCert), doormanCa.keyPair.private), CertPathAndKey(listOf(doormanCa.certificate, rootCaCert), doormanCa.keyPair.private),
DoormanConfig(approveAll = true, jira = null, approveInterval = timeoutMillis),
null,
if (startNetworkMap) { if (startNetworkMap) {
NetworkMapStartParams( NetworkMapStartParams(
LocalSigner(networkMapCa), LocalSigner(networkMapCa),
@ -161,7 +160,11 @@ class NetworkParametersUpdateTest : IntegrationTest() {
private fun applyNetworkParametersAndStart(networkParametersCmd: NetworkParametersCmd) { private fun applyNetworkParametersAndStart(networkParametersCmd: NetworkParametersCmd) {
server?.close() server?.close()
NetworkManagementServer(makeTestDataSourceProperties(dbName), DatabaseConfig(runMigration = true)).use { NetworkManagementServer(
makeTestDataSourceProperties(dbName),
DatabaseConfig(runMigration = true),
DoormanConfig(approveAll = true, jira = null, approveInterval = timeoutMillis),
null).use {
it.processNetworkParameters(networkParametersCmd) it.processNetworkParameters(networkParametersCmd)
} }
server = startServer(startNetworkMap = true) server = startServer(startNetworkMap = true)

View File

@ -73,6 +73,18 @@ class NodeRegistrationTest : IntegrationTest() {
private var server: NetworkManagementServer? = null private var server: NetworkManagementServer? = null
private val doormanConfig: DoormanConfig get() = DoormanConfig(approveAll = true, jira = null, approveInterval = timeoutMillis)
private val revocationConfig: CertificateRevocationConfig
get() = CertificateRevocationConfig(
approveAll = true,
jira = null,
approveInterval = timeoutMillis,
crlCacheTimeout = timeoutMillis,
localSigning = CertificateRevocationConfig.LocalSigning(
crlEndpoint = URL("http://test.com/crl"),
crlUpdateInterval = timeoutMillis)
)
@Before @Before
fun init() { fun init() {
dbName = random63BitValue().toString() dbName = random63BitValue().toString()
@ -108,7 +120,6 @@ class NodeRegistrationTest : IntegrationTest() {
}, },
rootCert = rootCaCert rootCert = rootCaCert
) )
internalDriver( internalDriver(
portAllocation = portAllocation, portAllocation = portAllocation,
compatibilityZone = compatibilityZone, compatibilityZone = compatibilityZone,
@ -157,20 +168,10 @@ class NodeRegistrationTest : IntegrationTest() {
} }
private fun startServer(startNetworkMap: Boolean = true): NetworkManagementServer { private fun startServer(startNetworkMap: Boolean = true): NetworkManagementServer {
val server = NetworkManagementServer(makeTestDataSourceProperties(dbName), DatabaseConfig(runMigration = true)) val server = NetworkManagementServer(makeTestDataSourceProperties(dbName), DatabaseConfig(runMigration = true), doormanConfig, revocationConfig)
server.start( server.start(
serverAddress, serverAddress,
CertPathAndKey(listOf(doormanCa.certificate, rootCaCert), doormanCa.keyPair.private), CertPathAndKey(listOf(doormanCa.certificate, rootCaCert), doormanCa.keyPair.private),
DoormanConfig(approveAll = true, jira = null, approveInterval = timeoutMillis),
CertificateRevocationConfig(
approveAll = true,
jira = null,
approveInterval = timeoutMillis,
crlCacheTimeout = timeoutMillis,
localSigning = CertificateRevocationConfig.LocalSigning(
crlEndpoint = URL("http://test.com/crl"),
crlUpdateInterval = timeoutMillis)
),
if (startNetworkMap) { if (startNetworkMap) {
NetworkMapStartParams( NetworkMapStartParams(
LocalSigner(networkMapCa), LocalSigner(networkMapCa),
@ -185,7 +186,7 @@ class NodeRegistrationTest : IntegrationTest() {
private fun applyNetworkParametersAndStart(networkParametersCmd: NetworkParametersCmd) { private fun applyNetworkParametersAndStart(networkParametersCmd: NetworkParametersCmd) {
server?.close() server?.close()
NetworkManagementServer(makeTestDataSourceProperties(dbName), DatabaseConfig(runMigration = true)).use { NetworkManagementServer(makeTestDataSourceProperties(dbName), DatabaseConfig(runMigration = true), doormanConfig, revocationConfig).use {
it.processNetworkParameters(networkParametersCmd) it.processNetworkParameters(networkParametersCmd)
} }
server = startServer(startNetworkMap = true) server = startServer(startNetworkMap = true)

View File

@ -66,6 +66,20 @@ class SigningServiceIntegrationTest : HsmBaseTest() {
private lateinit var dbName: String private lateinit var dbName: String
private val doormanConfig: DoormanConfig get() = DoormanConfig(approveAll = true, approveInterval = 2.seconds.toMillis(), jira = null)
private val revocationConfig: CertificateRevocationConfig
get() = CertificateRevocationConfig(
approveAll = true,
jira = null,
crlCacheTimeout = 30.minutes.toMillis(),
approveInterval = 10.minutes.toMillis(),
localSigning = CertificateRevocationConfig.LocalSigning(
crlEndpoint = URL("http://test.com/crl"),
crlUpdateInterval = 2.hours.toMillis()
)
)
@Before @Before
fun setUp() { fun setUp() {
dbName = random63BitValue().toString() dbName = random63BitValue().toString()
@ -99,21 +113,10 @@ class SigningServiceIntegrationTest : HsmBaseTest() {
@Test @Test
fun `Signing service signs approved CSRs`() { fun `Signing service signs approved CSRs`() {
//Start doorman server //Start doorman server
NetworkManagementServer(makeTestDataSourceProperties(), DatabaseConfig(runMigration = true)).use { server -> NetworkManagementServer(makeTestDataSourceProperties(), DatabaseConfig(runMigration = true), doormanConfig, revocationConfig).use { server ->
server.start( server.start(
hostAndPort = NetworkHostAndPort(HOST, 0), hostAndPort = NetworkHostAndPort(HOST, 0),
csrCertPathAndKey = null, csrCertPathAndKey = null,
doormanConfig = DoormanConfig(approveAll = true, approveInterval = 2.seconds.toMillis(), jira = null),
revocationConfig = CertificateRevocationConfig(
approveAll = true,
jira = null,
crlCacheTimeout = 30.minutes.toMillis(),
approveInterval = 10.minutes.toMillis(),
localSigning = CertificateRevocationConfig.LocalSigning(
crlEndpoint = URL("http://test.com/crl"),
crlUpdateInterval = 2.hours.toMillis()
)
),
startNetworkMap = null) startNetworkMap = null)
val doormanHostAndPort = server.hostAndPort val doormanHostAndPort = server.hostAndPort
// Start Corda network registration. // Start Corda network registration.

View File

@ -14,6 +14,7 @@ import net.corda.core.crypto.SecureHash
import net.corda.core.identity.CordaX500Name import net.corda.core.identity.CordaX500Name
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import org.bouncycastle.pkcs.PKCS10CertificationRequest import org.bouncycastle.pkcs.PKCS10CertificationRequest
import java.security.PublicKey
import java.security.cert.CertPath import java.security.cert.CertPath
data class CertificateData(val certStatus: CertificateStatus, val certPath: CertPath) data class CertificateData(val certStatus: CertificateStatus, val certPath: CertPath)
@ -83,6 +84,14 @@ interface CertificateSigningRequestStorage {
* @throws IllegalArgumentException if request is not found or not in Approved state. * @throws IllegalArgumentException if request is not found or not in Approved state.
*/ */
fun putCertificatePath(requestId: String, certPath: CertPath, signedBy: String) fun putCertificatePath(requestId: String, certPath: CertPath, signedBy: String)
/**
* Retrieves the certificate path for the given public key hash if such exists and its certificate is considered to be valid.
*
* @param publicKey public key corresponding to the certificate being searched.
* @return certificate path for the given public key hash or null if such certificate does not exist or is not valid.
*/
fun getValidCertificatePath(publicKey: PublicKey): CertPath?
} }
sealed class CertificateResponse { sealed class CertificateResponse {

View File

@ -23,6 +23,7 @@ import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseTransaction import net.corda.nodeapi.internal.persistence.DatabaseTransaction
import net.corda.nodeapi.internal.persistence.TransactionIsolationLevel import net.corda.nodeapi.internal.persistence.TransactionIsolationLevel
import org.bouncycastle.pkcs.PKCS10CertificationRequest import org.bouncycastle.pkcs.PKCS10CertificationRequest
import java.security.PublicKey
import java.security.cert.CertPath import java.security.cert.CertPath
import java.time.Instant import java.time.Instant
import javax.security.auth.x500.X500Principal import javax.security.auth.x500.X500Principal
@ -66,7 +67,6 @@ class PersistentCertificateSigningRequestStorage(private val database: CordaPers
} catch (e: RequestValidationException) { } catch (e: RequestValidationException) {
e.rejectMessage e.rejectMessage
} }
val requestEntity = CertificateSigningRequestEntity( val requestEntity = CertificateSigningRequestEntity(
requestId = requestId, requestId = requestId,
legalName = legalNameOrRejectMessage as? CordaX500Name, legalName = legalNameOrRejectMessage as? CordaX500Name,
@ -131,6 +131,16 @@ class PersistentCertificateSigningRequestStorage(private val database: CordaPers
} }
} }
override fun getValidCertificatePath(publicKey: PublicKey): CertPath? {
return database.transaction {
session.createQuery(
"select a.certificateData.certPath from ${CertificateSigningRequestEntity::class.java.name} a " +
"where a.publicKeyHash = :publicKeyHash and a.status = 'DONE' and a.certificateData.certificateStatus = 'VALID'", CertPath::class.java)
.setParameter("publicKeyHash", publicKey.hashString())
.uniqueResult()
}
}
override fun getRequest(requestId: String): CertificateSigningRequest? { override fun getRequest(requestId: String): CertificateSigningRequest? {
return database.transaction { return database.transaction {
findRequest(requestId)?.toCertificateSigningRequest() findRequest(requestId)?.toCertificateSigningRequest()

View File

@ -87,7 +87,7 @@ private fun caKeyGenMode(config: NetworkManagementServerConfig) {
} }
private fun doormanMode(cmdLineOptions: DoormanCmdLineOptions, config: NetworkManagementServerConfig) { private fun doormanMode(cmdLineOptions: DoormanCmdLineOptions, config: NetworkManagementServerConfig) {
val networkManagementServer = NetworkManagementServer(config.dataSourceProperties, config.database) val networkManagementServer = NetworkManagementServer(config.dataSourceProperties, config.database, config.doorman, config.revocation)
if (cmdLineOptions.networkParametersCmd == null) { if (cmdLineOptions.networkParametersCmd == null) {
// TODO: move signing to signing server. // TODO: move signing to signing server.
@ -103,8 +103,6 @@ private fun doormanMode(cmdLineOptions: DoormanCmdLineOptions, config: NetworkMa
networkManagementServer.start( networkManagementServer.start(
config.address, config.address,
csrAndNetworkMap?.first, csrAndNetworkMap?.first,
config.doorman,
config.revocation,
networkMapStartParams) networkMapStartParams)
Runtime.getRuntime().addShutdownHook(object : Thread("ShutdownHook") { Runtime.getRuntime().addShutdownHook(object : Thread("ShutdownHook") {

View File

@ -21,7 +21,6 @@ import net.corda.core.node.NetworkParameters
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.DatabaseConfig
import java.io.Closeable import java.io.Closeable
import java.net.URI import java.net.URI
@ -31,7 +30,10 @@ import java.util.*
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class NetworkManagementServer(dataSourceProperties: Properties, databaseConfig: DatabaseConfig) : Closeable { class NetworkManagementServer(dataSourceProperties: Properties,
databaseConfig: DatabaseConfig,
private val doormanConfig: DoormanConfig?, // TODO Doorman config shouldn't be optional as the doorman is always required to run
private val revocationConfig: CertificateRevocationConfig?) : Closeable {
companion object { companion object {
private val logger = contextLogger() private val logger = contextLogger()
} }
@ -40,7 +42,21 @@ class NetworkManagementServer(dataSourceProperties: Properties, databaseConfig:
private val database = configureDatabase(dataSourceProperties, databaseConfig).also { closeActions += it::close } private val database = configureDatabase(dataSourceProperties, databaseConfig).also { closeActions += it::close }
private val networkMapStorage = PersistentNetworkMapStorage(database) private val networkMapStorage = PersistentNetworkMapStorage(database)
private val nodeInfoStorage = PersistentNodeInfoStorage(database) private val nodeInfoStorage = PersistentNodeInfoStorage(database)
private val crlStorage = PersistentCertificateRevocationListStorage(database)
private val csrStorage = doormanConfig?.let {
if (it.approveAll) {
ApproveAllCertificateSigningRequestStorage(PersistentCertificateSigningRequestStorage(database))
} else {
PersistentCertificateSigningRequestStorage(database)
}
}
private val crrStorage = revocationConfig?.let {
if (it.approveAll) {
ApproveAllCertificateRevocationRequestStorage(PersistentCertificateRevocationRequestStorage(database))
} else {
PersistentCertificateRevocationRequestStorage(database)
}
}
lateinit var hostAndPort: NetworkHostAndPort lateinit var hostAndPort: NetworkHostAndPort
override fun close() { override fun close() {
@ -55,6 +71,8 @@ class NetworkManagementServer(dataSourceProperties: Properties, databaseConfig:
} }
private fun getNetworkMapService(config: NetworkMapConfig, signer: LocalSigner?): NetworkMapWebService { private fun getNetworkMapService(config: NetworkMapConfig, signer: LocalSigner?): NetworkMapWebService {
logger.info("Starting Network Map server.")
csrStorage ?: throw IllegalStateException("Certificate signing request storage cannot be null when creating the network map service.")
val localNetworkMapSigner = signer?.let { NetworkMapSigner(networkMapStorage, it) } val localNetworkMapSigner = signer?.let { NetworkMapSigner(networkMapStorage, it) }
val latestParameters = networkMapStorage.getLatestNetworkParameters()?.networkParameters ?: val latestParameters = networkMapStorage.getLatestNetworkParameters()?.networkParameters ?:
throw IllegalStateException("No network parameters were found. Please upload new network parameters before starting network map service") throw IllegalStateException("No network parameters were found. Please upload new network parameters before starting network map service")
@ -75,28 +93,21 @@ class NetworkManagementServer(dataSourceProperties: Properties, databaseConfig:
closeActions += scheduledExecutor::shutdown closeActions += scheduledExecutor::shutdown
} }
return NetworkMapWebService(nodeInfoStorage, networkMapStorage, config) return NetworkMapWebService(nodeInfoStorage, networkMapStorage, csrStorage, config)
} }
private fun getDoormanService(config: DoormanConfig, private fun getDoormanService(config: DoormanConfig,
database: CordaPersistence,
csrCertPathAndKey: CertPathAndKey?, csrCertPathAndKey: CertPathAndKey?,
serverStatus: NetworkManagementServerStatus): RegistrationWebService { serverStatus: NetworkManagementServerStatus): RegistrationWebService {
logger.info("Starting Doorman server.") logger.info("Starting Doorman server.")
val requestService = if (config.approveAll) { csrStorage ?: throw IllegalStateException("Certificate signing request storage cannot be null when creating the doorman service.")
logger.warn("Doorman server is in 'Approve All' mode, this will approve all incoming certificate signing requests.")
ApproveAllCertificateSigningRequestStorage(PersistentCertificateSigningRequestStorage(database))
} else {
PersistentCertificateSigningRequestStorage(database)
}
val jiraConfig = config.jira val jiraConfig = config.jira
val requestProcessor = if (jiraConfig != null) { val requestProcessor = if (jiraConfig != null) {
val jiraWebAPI = AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(URI(jiraConfig.address), jiraConfig.username, jiraConfig.password) val jiraWebAPI = AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(URI(jiraConfig.address), jiraConfig.username, jiraConfig.password)
val jiraClient = CsrJiraClient(jiraWebAPI, jiraConfig.projectCode) val jiraClient = CsrJiraClient(jiraWebAPI, jiraConfig.projectCode)
JiraCsrHandler(jiraClient, requestService, DefaultCsrHandler(requestService, csrCertPathAndKey)) JiraCsrHandler(jiraClient, csrStorage, DefaultCsrHandler(csrStorage, csrCertPathAndKey))
} else { } else {
DefaultCsrHandler(requestService, csrCertPathAndKey) DefaultCsrHandler(csrStorage, csrCertPathAndKey)
} }
val scheduledExecutor = Executors.newScheduledThreadPool(1) val scheduledExecutor = Executors.newScheduledThreadPool(1)
@ -117,17 +128,9 @@ class NetworkManagementServer(dataSourceProperties: Properties, databaseConfig:
} }
private fun getRevocationServices(config: CertificateRevocationConfig, private fun getRevocationServices(config: CertificateRevocationConfig,
database: CordaPersistence,
csrCertPathAndKeyPair: CertPathAndKey?): Pair<CertificateRevocationRequestWebService, CertificateRevocationListWebService> { csrCertPathAndKeyPair: CertPathAndKey?): Pair<CertificateRevocationRequestWebService, CertificateRevocationListWebService> {
logger.info("Starting Revocation server.") logger.info("Starting Revocation server.")
val crrStorage = if (config.approveAll) { crrStorage ?: throw IllegalStateException("Certificate revocation request storage cannot be null when creating the revocation service.")
logger.warn("Revocation server is in 'Approve All' mode, this will approve all incoming certificate signing requests.")
ApproveAllCertificateRevocationRequestStorage(PersistentCertificateRevocationRequestStorage(database))
} else {
PersistentCertificateRevocationRequestStorage(database)
}
val crlStorage = PersistentCertificateRevocationListStorage(database)
val crlHandler = csrCertPathAndKeyPair?.let { val crlHandler = csrCertPathAndKeyPair?.let {
LocalCrlHandler(crrStorage, LocalCrlHandler(crrStorage,
crlStorage, crlStorage,
@ -163,17 +166,15 @@ class NetworkManagementServer(dataSourceProperties: Properties, databaseConfig:
fun start(hostAndPort: NetworkHostAndPort, fun start(hostAndPort: NetworkHostAndPort,
csrCertPathAndKey: CertPathAndKey?, csrCertPathAndKey: CertPathAndKey?,
doormanConfig: DoormanConfig?, // TODO Doorman config shouldn't be optional as the doorman is always required to run
revocationConfig: CertificateRevocationConfig?,
startNetworkMap: NetworkMapStartParams? startNetworkMap: NetworkMapStartParams?
) { ) {
val services = mutableListOf<Any>() val services = mutableListOf<Any>()
val serverStatus = NetworkManagementServerStatus() val serverStatus = NetworkManagementServerStatus()
startNetworkMap?.let { services += getNetworkMapService(it.config, it.signer) } startNetworkMap?.let { services += getNetworkMapService(it.config, it.signer) }
doormanConfig?.let { services += getDoormanService(it, database, csrCertPathAndKey, serverStatus) } doormanConfig?.let { services += getDoormanService(it, csrCertPathAndKey, serverStatus) }
revocationConfig?.let { revocationConfig?.let {
val revocationServices = getRevocationServices(it, database, csrCertPathAndKey) val revocationServices = getRevocationServices(it, csrCertPathAndKey)
services += revocationServices.first services += revocationServices.first
services += revocationServices.second services += revocationServices.second
} }

View File

@ -12,6 +12,7 @@ package com.r3.corda.networkmanage.doorman.webservice
import com.github.benmanes.caffeine.cache.Caffeine import com.github.benmanes.caffeine.cache.Caffeine
import com.github.benmanes.caffeine.cache.LoadingCache import com.github.benmanes.caffeine.cache.LoadingCache
import com.r3.corda.networkmanage.common.persistence.CertificateSigningRequestStorage
import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage
import com.r3.corda.networkmanage.common.persistence.NodeInfoStorage import com.r3.corda.networkmanage.common.persistence.NodeInfoStorage
import com.r3.corda.networkmanage.doorman.NetworkMapConfig import com.r3.corda.networkmanage.doorman.NetworkMapConfig
@ -20,6 +21,7 @@ import net.corda.core.crypto.CompositeKey
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.SignedData import net.corda.core.crypto.SignedData
import net.corda.core.crypto.sha256 import net.corda.core.crypto.sha256
import net.corda.core.internal.CertRole
import net.corda.core.node.NetworkParameters import net.corda.core.node.NetworkParameters
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.serialization.deserialize import net.corda.core.serialization.deserialize
@ -29,10 +31,14 @@ import net.corda.core.utilities.debug
import net.corda.core.utilities.trace import net.corda.core.utilities.trace
import net.corda.nodeapi.internal.NodeInfoAndSigned import net.corda.nodeapi.internal.NodeInfoAndSigned
import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.SignedNodeInfo
import net.corda.nodeapi.internal.crypto.X509Utilities.validateCertPath
import net.corda.nodeapi.internal.crypto.x509
import net.corda.nodeapi.internal.crypto.x509Certificates
import net.corda.nodeapi.internal.network.SignedNetworkMap import net.corda.nodeapi.internal.network.SignedNetworkMap
import java.io.InputStream import java.io.InputStream
import java.security.InvalidKeyException import java.security.InvalidKeyException
import java.security.SignatureException import java.security.SignatureException
import java.security.cert.CertPathValidatorException
import java.time.Duration import java.time.Duration
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
@ -46,6 +52,7 @@ import javax.ws.rs.core.Response.status
@Path(NETWORK_MAP_PATH) @Path(NETWORK_MAP_PATH)
class NetworkMapWebService(private val nodeInfoStorage: NodeInfoStorage, class NetworkMapWebService(private val nodeInfoStorage: NodeInfoStorage,
private val networkMapStorage: NetworkMapStorage, private val networkMapStorage: NetworkMapStorage,
private val certificateSigningRequestStorage: CertificateSigningRequestStorage,
private val config: NetworkMapConfig) { private val config: NetworkMapConfig) {
companion object { companion object {
@ -91,7 +98,7 @@ class NetworkMapWebService(private val nodeInfoStorage: NodeInfoStorage,
logger.warn("Unable to process node-info: $nodeInfo", e) logger.warn("Unable to process node-info: $nodeInfo", e)
when (e) { when (e) {
is NetworkMapNotInitialisedException -> status(Response.Status.SERVICE_UNAVAILABLE).entity(e.message) is NetworkMapNotInitialisedException -> status(Response.Status.SERVICE_UNAVAILABLE).entity(e.message)
is InvalidPlatformVersionException -> status(Response.Status.BAD_REQUEST).entity(e.message) is RequestException -> status(Response.Status.BAD_REQUEST).entity(e.message)
is InvalidKeyException, is SignatureException -> status(Response.Status.UNAUTHORIZED).entity(e.message) 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. // Rethrow e if its not one of the expected exception, the server will return http 500 internal error.
else -> throw e else -> throw e
@ -154,11 +161,12 @@ class NetworkMapWebService(private val nodeInfoStorage: NodeInfoStorage,
} }
private fun verifyNodeInfo(nodeInfo: NodeInfo) { private fun verifyNodeInfo(nodeInfo: NodeInfo) {
checkCertificates(nodeInfo)
checkCompositeKeys(nodeInfo) checkCompositeKeys(nodeInfo)
val minimumPlatformVersion = currentNetworkParameters?.minimumPlatformVersion val minimumPlatformVersion = currentNetworkParameters?.minimumPlatformVersion
?: throw NetworkMapNotInitialisedException("Network parameters have not been initialised") ?: throw NetworkMapNotInitialisedException("Network parameters have not been initialised")
if (nodeInfo.platformVersion < minimumPlatformVersion) { if (nodeInfo.platformVersion < minimumPlatformVersion) {
throw InvalidPlatformVersionException("Minimum platform version is $minimumPlatformVersion") throw RequestException("Minimum platform version is $minimumPlatformVersion")
} }
} }
@ -169,7 +177,9 @@ class NetworkMapWebService(private val nodeInfoStorage: NodeInfoStorage,
} }
val parameters = checkNotNull(currentNetworkParameters) { "Network parameters not available." } val parameters = checkNotNull(currentNetworkParameters) { "Network parameters not available." }
val notaryIdentities = parameters.notaries.map { it.identity } val notaryIdentities = parameters.notaries.map { it.identity }
require(notaryIdentities.containsAll(compositeKeyIdentities)) { "A composite key needs to belong to a notary." } if (!notaryIdentities.containsAll(compositeKeyIdentities)) {
throw RequestException("A composite key needs to belong to a notary.")
}
} }
private fun createResponse(payload: Any?, addCacheTimeout: Boolean = false): Response { private fun createResponse(payload: Any?, addCacheTimeout: Boolean = false): Response {
@ -184,8 +194,23 @@ class NetworkMapWebService(private val nodeInfoStorage: NodeInfoStorage,
}.build() }.build()
} }
private fun checkCertificates(nodeInfo: NodeInfo) {
val nodeCaCert = nodeInfo.legalIdentitiesAndCerts.first().certPath.x509Certificates.find { CertRole.extract(it) == CertRole.NODE_CA }
nodeCaCert ?: throw RequestException("The node certificate path does not contain the node CA certificate type in it.")
val nodeCertPath = certificateSigningRequestStorage.getValidCertificatePath(nodeCaCert.publicKey)
nodeCertPath ?: throw RequestException("Node certificate is either no longer valid or was never registered.")
val rootCert = nodeCertPath.certificates.last().x509
try {
nodeInfo.legalIdentitiesAndCerts.forEach {
validateCertPath(rootCert, it.certPath)
}
} catch (e: CertPathValidatorException) {
throw RequestException("Invalid certificate path.")
}
}
class NetworkMapNotInitialisedException(message: String?) : Exception(message) class NetworkMapNotInitialisedException(message: String?) : Exception(message)
class InvalidPlatformVersionException(message: String?) : Exception(message) class RequestException(message: String) : Exception(message)
private data class CachedData(val signedNetworkMap: SignedNetworkMap, private data class CachedData(val signedNetworkMap: SignedNetworkMap,
val nodeInfoHashes: Set<SecureHash>, val nodeInfoHashes: Set<SecureHash>,

View File

@ -124,6 +124,39 @@ class PersistentCertificateRequestStorageTest : TestBase() {
assertNotNull(storage.getRequest(requestId)!!.certData) assertNotNull(storage.getRequest(requestId)!!.certData)
} }
@Test
fun `get valid certificate path returns correct value`() {
// given
val (csr, nodeKeyPair) = createRequest("LegalName", certRole = CertRole.NODE_CA)
val requestId = storage.saveRequest(csr)
storage.markRequestTicketCreated(requestId)
storage.approveRequest(requestId, DOORMAN_SIGNATURE)
val certPath = generateSignedCertPath(csr, nodeKeyPair)
storage.putCertificatePath(
requestId,
certPath,
DOORMAN_SIGNATURE
)
// when
val storedCertPath = storage.getValidCertificatePath(nodeKeyPair.public)
// then
assertEquals(certPath, storedCertPath)
}
@Test
fun `get valid certificate path returns null if the certificate path cannot be found`() {
// given
val (_, nodeKeyPair) = createRequest("LegalName", certRole = CertRole.NODE_CA)
// when
val storedCertPath = storage.getValidCertificatePath(nodeKeyPair.public)
// then
assertNull(storedCertPath)
}
@Test @Test
fun `sign request ignores subsequent sign requests`() { fun `sign request ignores subsequent sign requests`() {
val (csr, nodeKeyPair) = createRequest("LegalName", certRole = CertRole.NODE_CA) val (csr, nodeKeyPair) = createRequest("LegalName", certRole = CertRole.NODE_CA)

View File

@ -11,6 +11,7 @@
package com.r3.corda.networkmanage.doorman.webservice package com.r3.corda.networkmanage.doorman.webservice
import com.nhaarman.mockito_kotlin.* import com.nhaarman.mockito_kotlin.*
import com.r3.corda.networkmanage.common.persistence.CertificateSigningRequestStorage
import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage
import com.r3.corda.networkmanage.common.persistence.NodeInfoStorage import com.r3.corda.networkmanage.common.persistence.NodeInfoStorage
import com.r3.corda.networkmanage.createNetworkMapEntity import com.r3.corda.networkmanage.createNetworkMapEntity
@ -42,6 +43,7 @@ import java.io.IOException
import java.net.URL import java.net.URL
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
class NetworkMapWebServiceTest { class NetworkMapWebServiceTest {
@Rule @Rule
@ -62,28 +64,54 @@ class NetworkMapWebServiceTest {
@Test @Test
fun `submit nodeInfo`() { fun `submit nodeInfo`() {
// Create node info.
val (_, signedNodeInfo) = createNodeInfoAndSigned(CordaX500Name("Test", "London", "GB"))
val networkMapStorage: NetworkMapStorage = mock { val networkMapStorage: NetworkMapStorage = mock {
on { getActiveNetworkMap() }.thenReturn(createNetworkMapEntity()) on { getActiveNetworkMap() }.thenReturn(createNetworkMapEntity())
} }
// Create node info. val csrStorage: CertificateSigningRequestStorage = mock {
val (_, signedNodeInfo) = createNodeInfoAndSigned(CordaX500Name("Test", "London", "GB")) on { getValidCertificatePath(any()) }.thenReturn(signedNodeInfo.verified().legalIdentitiesAndCerts.first().certPath)
}
NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(mock(), networkMapStorage, testNetworkMapConfig)).use { NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(mock(), networkMapStorage, csrStorage, testNetworkMapConfig)).use {
it.start() it.start()
// Post node info and signature to doorman, this should pass without any exception. // Post node info and signature to doorman, this should pass without any exception.
it.doPost("publish", signedNodeInfo.serialize()) it.doPost("publish", signedNodeInfo.serialize())
} }
} }
@Test
fun `submit nodeInfo with an unknown public key fails`() {
// Create node info.
val (_, signedNodeInfo) = createNodeInfoAndSigned(CordaX500Name("Test", "London", "GB"))
val networkMapStorage: NetworkMapStorage = mock {
on { getActiveNetworkMap() }.thenReturn(createNetworkMapEntity())
}
val csrStorage: CertificateSigningRequestStorage = mock {
on { getValidCertificatePath(any()) }.thenReturn(null)
}
NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(mock(), networkMapStorage, csrStorage, testNetworkMapConfig)).use {
it.start()
// Post node info and signature to doorman, this should pass without any exception.
assertFailsWith<IOException>("Response Code 400") {
it.doPost("publish", signedNodeInfo.serialize())
}
}
}
@Test @Test
fun `submit old nodeInfo`() { fun `submit old nodeInfo`() {
// Create node info.
val (_, signedNodeInfo) = createNodeInfoAndSigned(CordaX500Name("Test", "London", "GB"), platformVersion = 1)
val networkMapStorage: NetworkMapStorage = mock { val networkMapStorage: NetworkMapStorage = mock {
on { getActiveNetworkMap() }.thenReturn(createNetworkMapEntity(networkParameters = testNetworkParameters(minimumPlatformVersion = 2))) on { getActiveNetworkMap() }.thenReturn(createNetworkMapEntity(networkParameters = testNetworkParameters(minimumPlatformVersion = 2)))
} }
// Create node info. val csrStorage: CertificateSigningRequestStorage = mock {
val (_, signedNodeInfo) = createNodeInfoAndSigned(CordaX500Name("Test", "London", "GB"), platformVersion = 1) on { getValidCertificatePath(any()) }.thenReturn(signedNodeInfo.verified().legalIdentitiesAndCerts.first().certPath)
}
NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(mock(), networkMapStorage, testNetworkMapConfig)).use { NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(mock(), networkMapStorage, csrStorage, testNetworkMapConfig)).use {
it.start() it.start()
assertThatThrownBy { it.doPost("publish", signedNodeInfo.serialize()) } assertThatThrownBy { it.doPost("publish", signedNodeInfo.serialize()) }
.hasMessageStartingWith("Response Code 400: Minimum platform version is 2") .hasMessageStartingWith("Response Code 400: Minimum platform version is 2")
@ -92,13 +120,16 @@ class NetworkMapWebServiceTest {
@Test @Test
fun `submit nodeInfo when no network map`() { fun `submit nodeInfo when no network map`() {
// Create node info.
val (_, signedNodeInfo) = createNodeInfoAndSigned(CordaX500Name("Test", "London", "GB"), platformVersion = 1)
val networkMapStorage: NetworkMapStorage = mock { val networkMapStorage: NetworkMapStorage = mock {
on { getActiveNetworkMap() }.thenReturn(null) on { getActiveNetworkMap() }.thenReturn(null)
} }
// Create node info. val csrStorage: CertificateSigningRequestStorage = mock {
val (_, signedNodeInfo) = createNodeInfoAndSigned(CordaX500Name("Test", "London", "GB"), platformVersion = 1) on { getValidCertificatePath(any()) }.thenReturn(signedNodeInfo.verified().legalIdentitiesAndCerts.first().certPath)
}
NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(mock(), networkMapStorage, testNetworkMapConfig)).use { NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(mock(), networkMapStorage, csrStorage, testNetworkMapConfig)).use {
it.start() it.start()
assertThatThrownBy { it.doPost("publish", signedNodeInfo.serialize()) } assertThatThrownBy { it.doPost("publish", signedNodeInfo.serialize()) }
.hasMessageStartingWith("Response Code 503: Network parameters have not been initialised") .hasMessageStartingWith("Response Code 503: Network parameters have not been initialised")
@ -115,7 +146,7 @@ class NetworkMapWebServiceTest {
on { getActiveNetworkMap() }.thenReturn(networkMapEntity) on { getActiveNetworkMap() }.thenReturn(networkMapEntity)
} }
NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(mock(), networkMapStorage, testNetworkMapConfig)).use { NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(mock(), networkMapStorage, mock(), testNetworkMapConfig)).use {
it.start() it.start()
val signedNetworkMapResponse = it.doGet<SignedNetworkMap>("") val signedNetworkMapResponse = it.doGet<SignedNetworkMap>("")
verify(networkMapStorage, times(1)).getActiveNetworkMap() verify(networkMapStorage, times(1)).getActiveNetworkMap()
@ -137,7 +168,7 @@ class NetworkMapWebServiceTest {
on { getActiveNetworkMap() }.thenReturn(createNetworkMapEntity(nodeInfoHashes = listOf(nodeInfoHash))) on { getActiveNetworkMap() }.thenReturn(createNetworkMapEntity(nodeInfoHashes = listOf(nodeInfoHash)))
} }
NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(nodeInfoStorage, networkMapStorage, testNetworkMapConfig)).use { NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(nodeInfoStorage, networkMapStorage, mock(), testNetworkMapConfig)).use {
it.start() it.start()
val nodeInfoResponse = it.doGet<SignedNodeInfo>("node-info/$nodeInfoHash") val nodeInfoResponse = it.doGet<SignedNodeInfo>("node-info/$nodeInfoHash")
verify(nodeInfoStorage, times(1)).getNodeInfo(nodeInfoHash) verify(nodeInfoStorage, times(1)).getNodeInfo(nodeInfoHash)
@ -159,7 +190,7 @@ class NetworkMapWebServiceTest {
on { getSignedNetworkParameters(networkParametersHash) }.thenReturn(signedNetworkParameters) on { getSignedNetworkParameters(networkParametersHash) }.thenReturn(signedNetworkParameters)
} }
NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(mock(), networkMapStorage, testNetworkMapConfig)).use { NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(mock(), networkMapStorage, mock(), testNetworkMapConfig)).use {
it.start() it.start()
val netParamsResponse = it.doGet<SignedNetworkParameters>("network-parameters/$networkParametersHash") val netParamsResponse = it.doGet<SignedNetworkParameters>("network-parameters/$networkParametersHash")
verify(networkMapStorage, times(1)).getSignedNetworkParameters(networkParametersHash) verify(networkMapStorage, times(1)).getSignedNetworkParameters(networkParametersHash)
@ -181,7 +212,7 @@ class NetworkMapWebServiceTest {
val networkMapStorage: NetworkMapStorage = mock { val networkMapStorage: NetworkMapStorage = mock {
on { getSignedNetworkParameters(hash) }.thenReturn(signingCertAndKeyPair.sign(netParams)) on { getSignedNetworkParameters(hash) }.thenReturn(signingCertAndKeyPair.sign(netParams))
} }
NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(nodeInfoStorage, networkMapStorage, testNetworkMapConfig)).use { NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(nodeInfoStorage, networkMapStorage, mock(), testNetworkMapConfig)).use {
it.start() it.start()
val keyPair = Crypto.generateKeyPair() val keyPair = Crypto.generateKeyPair()
val signedHash = hash.serialize().sign { keyPair.sign(it) } val signedHash = hash.serialize().sign { keyPair.sign(it) }