ENT-1226 Check minimum platform version when submitting node info (#300)

* Adding min platform version check when submitting node info

* Return error message to developer

* Fixing integration test

* Added todo to move checks out of data layer

* Added extra logging in nodeinfowebservice and use nodeinfo.verified instead of deserialize

* Tidy up tests

* Cache network parameters

* Add NodeInfoWithSigned class to stop calling to only verify node data once

* Fixing review comments

* Return correct response code if doorman not initialised properly

* Fix merge conflict
This commit is contained in:
Anthony Keenan 2018-01-09 13:31:26 +00:00 committed by GitHub
parent 2832eb489b
commit a65db712c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 118 additions and 44 deletions

View File

@ -1,7 +1,6 @@
package com.r3.corda.networkmanage.common.persistence
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.SignedData
import net.corda.core.node.NodeInfo
import net.corda.nodeapi.internal.SignedNodeInfo
import java.security.cert.CertPath
@ -27,5 +26,5 @@ interface NodeInfoStorage {
* @param signedNodeInfo signed node info data to be stored
* @return hash for the newly created node info entry
*/
fun putNodeInfo(signedNodeInfo: SignedNodeInfo): SecureHash
fun putNodeInfo(signedNodeInfo: NodeInfoWithSigned): SecureHash
}

View File

@ -0,0 +1,8 @@
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

@ -17,10 +17,12 @@ import java.security.cert.CertPath
* Database implementation of the [NetworkMapStorage] interface
*/
class PersistentNodeInfoStorage(private val database: CordaPersistence) : NodeInfoStorage {
override fun putNodeInfo(signedNodeInfo: SignedNodeInfo): SecureHash {
val nodeInfo = signedNodeInfo.verified()
override fun putNodeInfo(nodeInfoWithSigned: NodeInfoWithSigned): SecureHash {
val nodeInfo = nodeInfoWithSigned.nodeInfo
val signedNodeInfo = nodeInfoWithSigned.signedNodeInfo
val nodeCaCert = nodeInfo.legalIdentitiesAndCerts[0].certPath.certificates.find { CertRole.extract(it) == CertRole.NODE_CA }
return database.transaction(TransactionIsolationLevel.SERIALIZABLE) {
// TODO Move these checks out of data access layer
val request = nodeCaCert?.let {
singleRequestWhere(CertificateDataEntity::class.java) { builder, path ->
val certPublicKeyHashEq = builder.equal(path.get<String>(CertificateDataEntity::publicKeyHash.name), it.publicKey.encoded.sha256().toString())

View File

@ -5,12 +5,16 @@ 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.doorman.NetworkMapConfig
import com.r3.corda.networkmanage.doorman.webservice.NodeInfoWebService.Companion.NETWORK_MAP_PATH
import net.corda.core.crypto.SecureHash
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.nodeapi.internal.SignedNodeInfo
import net.corda.nodeapi.internal.network.NetworkParameters
import net.corda.nodeapi.internal.network.SignedNetworkMap
import java.io.InputStream
import java.security.InvalidKeyException
@ -29,35 +33,41 @@ import javax.ws.rs.core.Response.status
class NodeInfoWebService(private val nodeInfoStorage: NodeInfoStorage,
private val networkMapStorage: NetworkMapStorage,
private val config: NetworkMapConfig) {
companion object {
val log = contextLogger()
const val NETWORK_MAP_PATH = "network-map"
}
private val networkMapCache: LoadingCache<Boolean, SignedNetworkMap?> = CacheBuilder.newBuilder()
private val networkMapCache: LoadingCache<Boolean, Pair<SignedNetworkMap?, NetworkParameters?>> = CacheBuilder.newBuilder()
.expireAfterWrite(config.cacheTimeout, TimeUnit.MILLISECONDS)
.build(CacheLoader.from { _ -> networkMapStorage.getCurrentNetworkMap() })
.build(CacheLoader.from { _ -> Pair(networkMapStorage.getCurrentNetworkMap(), networkMapStorage.getCurrentNetworkParameters()) })
@POST
@Path("publish")
@Consumes(MediaType.APPLICATION_OCTET_STREAM)
fun registerNode(input: InputStream): Response {
val registrationData = input.readBytes().deserialize<SignedNodeInfo>()
val signedNodeInfo = input.readBytes().deserialize<SignedNodeInfo>()
return try {
// Store the NodeInfo
nodeInfoStorage.putNodeInfo(registrationData)
val nodeInfoWithSignature = NodeInfoWithSigned(signedNodeInfo)
verifyNodeInfo(nodeInfoWithSignature.nodeInfo)
nodeInfoStorage.putNodeInfo(nodeInfoWithSignature)
ok()
} catch (e: Exception) {
// Catch exceptions thrown by signature verification.
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.
// Rethrow e if its not one of the expected exception, the server will return http 500 internal error.
else -> throw e
}
}.build()
}
@GET
fun getNetworkMap(): Response = createResponse(networkMapCache.get(true), addCacheTimeout = true)
fun getNetworkMap(): Response = createResponse(networkMapCache.get(true).first, addCacheTimeout = true)
@GET
@Path("node-info/{nodeInfoHash}")
@ -80,6 +90,18 @@ class NodeInfoWebService(private val nodeInfoStorage: NodeInfoStorage,
return ok(request.getHeader("X-Forwarded-For")?.split(",")?.first() ?: "${request.remoteHost}:${request.remotePort}").build()
}
private fun verifyNodeInfo(nodeInfo: NodeInfo) {
val minimumPlatformVersion = networkMapCache.get(true).second?.minimumPlatformVersion
if (minimumPlatformVersion == null) {
log.error("Network parameters have not been initialised")
throw NetworkMapNotInitialisedException("Network parameters have not been initialised")
}
if (nodeInfo.platformVersion < minimumPlatformVersion) {
log.error("Minimum platform version is $minimumPlatformVersion")
throw InvalidPlatformVersionException("Minimum platform version is $minimumPlatformVersion")
}
}
private fun createResponse(payload: Any?, addCacheTimeout: Boolean = false): Response {
return if (payload != null) {
val ok = Response.ok(payload.serialize().bytes)
@ -91,4 +113,7 @@ class NodeInfoWebService(private val nodeInfoStorage: NodeInfoStorage,
status(Response.Status.NOT_FOUND)
}.build()
}
class NetworkMapNotInitialisedException(message: String?) : Exception(message)
class InvalidPlatformVersionException(message: String?) : Exception(message)
}

View File

@ -148,7 +148,7 @@ class PersistentNetworkMapStorageTest : TestBase() {
assertThat(validNodeInfoHash).containsOnly(nodeInfoHashA, nodeInfoHashB)
}
private fun createValidSignedNodeInfo(organisation: String): SignedNodeInfo {
private fun createValidSignedNodeInfo(organisation: String): NodeInfoWithSigned {
val nodeInfoBuilder = TestNodeInfoBuilder()
val requestId = requestStorage.saveRequest(createRequest(organisation).first)
requestStorage.markRequestTicketCreated(requestId)
@ -156,6 +156,6 @@ class PersistentNetworkMapStorageTest : TestBase() {
val (identity) = nodeInfoBuilder.addIdentity(CordaX500Name(organisation, "London", "GB"))
val nodeCaCertPath = X509CertificateFactory().generateCertPath(identity.certPath.certificates.drop(1))
requestStorage.putCertificatePath(requestId, nodeCaCertPath, emptyList())
return nodeInfoBuilder.buildWithSigned().second
return NodeInfoWithSigned(nodeInfoBuilder.buildWithSigned().second)
}
}

View File

@ -28,7 +28,7 @@ import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
class PersitenceNodeInfoStorageTest : TestBase() {
class PersistentNodeInfoStorageTest : TestBase() {
private lateinit var requestStorage: CertificationRequestStorage
private lateinit var nodeInfoStorage: PersistentNodeInfoStorage
private lateinit var persistence: CordaPersistence
@ -85,34 +85,34 @@ class PersitenceNodeInfoStorageTest : TestBase() {
@Test
fun `getNodeInfo returns persisted SignedNodeInfo using the hash of just the NodeInfo`() {
// given
val (nodeInfoA, signedNodeInfoA) = createValidSignedNodeInfo("TestA")
val (nodeInfoB, signedNodeInfoB) = createValidSignedNodeInfo("TestB")
val (nodeA) = createValidSignedNodeInfo("TestA")
val (nodeB) = createValidSignedNodeInfo("TestB")
// Put signed node info data
nodeInfoStorage.putNodeInfo(signedNodeInfoA)
nodeInfoStorage.putNodeInfo(signedNodeInfoB)
nodeInfoStorage.putNodeInfo(nodeA)
nodeInfoStorage.putNodeInfo(nodeB)
// when
val persistedSignedNodeInfoA = nodeInfoStorage.getNodeInfo(nodeInfoA.serialize().hash)
val persistedSignedNodeInfoB = nodeInfoStorage.getNodeInfo(nodeInfoB.serialize().hash)
val persistedSignedNodeInfoA = nodeInfoStorage.getNodeInfo(nodeA.nodeInfo.serialize().hash)
val persistedSignedNodeInfoB = nodeInfoStorage.getNodeInfo(nodeB.nodeInfo.serialize().hash)
// then
assertEquals(persistedSignedNodeInfoA?.verified(), nodeInfoA)
assertEquals(persistedSignedNodeInfoB?.verified(), nodeInfoB)
assertEquals(persistedSignedNodeInfoA?.verified(), nodeA.nodeInfo)
assertEquals(persistedSignedNodeInfoB?.verified(), nodeB.nodeInfo)
}
@Test
fun `same public key with different node info`() {
// Create node info.
val (nodeInfo1, signedNodeInfo1, key) = createValidSignedNodeInfo("Test", serial = 1)
val nodeInfo2 = nodeInfo1.copy(serial = 2)
val signedNodeInfo2 = nodeInfo2.signWith(listOf(key))
val (node1, key) = createValidSignedNodeInfo("Test", serial = 1)
val nodeInfo2 = node1.nodeInfo.copy(serial = 2)
val node2 = NodeInfoWithSigned(nodeInfo2.signWith(listOf(key)))
val nodeInfo1Hash = nodeInfoStorage.putNodeInfo(signedNodeInfo1)
assertEquals(nodeInfo1, nodeInfoStorage.getNodeInfo(nodeInfo1Hash)?.verified())
val nodeInfo1Hash = nodeInfoStorage.putNodeInfo(node1)
assertEquals(node1.nodeInfo, nodeInfoStorage.getNodeInfo(nodeInfo1Hash)?.verified())
// This should replace the node info.
nodeInfoStorage.putNodeInfo(signedNodeInfo2)
nodeInfoStorage.putNodeInfo(node2)
// Old node info should be removed.
assertNull(nodeInfoStorage.getNodeInfo(nodeInfo1Hash))
@ -122,17 +122,17 @@ class PersitenceNodeInfoStorageTest : TestBase() {
@Test
fun `putNodeInfo persists SignedNodeInfo with its signature`() {
// given
val (_, signedNodeInfo) = createValidSignedNodeInfo("Test")
val (nodeInfoWithSigned) = createValidSignedNodeInfo("Test")
// when
val nodeInfoHash = nodeInfoStorage.putNodeInfo(signedNodeInfo)
val nodeInfoHash = nodeInfoStorage.putNodeInfo(nodeInfoWithSigned)
// then
val persistedSignedNodeInfo = nodeInfoStorage.getNodeInfo(nodeInfoHash)
assertThat(persistedSignedNodeInfo?.signatures).isEqualTo(signedNodeInfo.signatures)
assertThat(persistedSignedNodeInfo?.signatures).isEqualTo(nodeInfoWithSigned.signedNodeInfo.signatures)
}
private fun createValidSignedNodeInfo(organisation: String, serial: Long = 1): Triple<NodeInfo, SignedNodeInfo, PrivateKey> {
private fun createValidSignedNodeInfo(organisation: String, serial: Long = 1): Pair<NodeInfoWithSigned, PrivateKey> {
val nodeInfoBuilder = TestNodeInfoBuilder()
val requestId = requestStorage.saveRequest(createRequest(organisation).first)
requestStorage.markRequestTicketCreated(requestId)
@ -140,7 +140,7 @@ class PersitenceNodeInfoStorageTest : TestBase() {
val (identity, key) = nodeInfoBuilder.addIdentity(CordaX500Name(organisation, "London", "GB"))
val nodeCaCertPath = X509CertificateFactory().generateCertPath(identity.certPath.certificates.drop(1))
requestStorage.putCertificatePath(requestId, nodeCaCertPath, emptyList())
val (nodeInfo, signedNodeInfo) = nodeInfoBuilder.buildWithSigned(serial)
return Triple(nodeInfo, signedNodeInfo, key)
val (_, signedNodeInfo) = nodeInfoBuilder.buildWithSigned(serial)
return Pair(NodeInfoWithSigned(signedNodeInfo), key)
}
}

View File

@ -25,13 +25,15 @@ import net.corda.testing.SerializationEnvironmentRule
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.internal.createDevIntermediateCaCertPath
import net.corda.testing.internal.createNodeInfoAndSigned
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatExceptionOfType
import org.apache.commons.io.IOUtils
import org.assertj.core.api.Assertions.*
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import java.io.FileNotFoundException
import java.io.IOException
import java.net.URL
import java.nio.charset.Charset
import java.security.cert.X509Certificate
import javax.ws.rs.core.MediaType
import kotlin.test.assertEquals
@ -44,7 +46,7 @@ class NodeInfoWebServiceTest {
private lateinit var rootCaCert: X509Certificate
private lateinit var intermediateCa: CertificateAndKeyPair
private val testNetworkMapConfig = NetworkMapConfig(10.seconds.toMillis(), 10.seconds.toMillis())
private val testNetworkMapConfig = NetworkMapConfig(10.seconds.toMillis(), 10.seconds.toMillis())
@Before
fun init() {
@ -55,10 +57,13 @@ class NodeInfoWebServiceTest {
@Test
fun `submit nodeInfo`() {
val networkMapStorage: NetworkMapStorage = mock {
on { getCurrentNetworkParameters() }.thenReturn(testNetworkParameters(emptyList()))
}
// Create node info.
val (_, signedNodeInfo) = createNodeInfoAndSigned(CordaX500Name("Test", "London", "GB"))
NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(mock(), mock(), testNetworkMapConfig)).use {
NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(mock(), networkMapStorage, testNetworkMapConfig)).use {
it.start()
val nodeInfoAndSignature = signedNodeInfo.serialize().bytes
// Post node info and signature to doorman, this should pass without any exception.
@ -66,6 +71,38 @@ class NodeInfoWebServiceTest {
}
}
@Test
fun `submit old nodeInfo`() {
val networkMapStorage: NetworkMapStorage = mock {
on { getCurrentNetworkParameters() }.thenReturn(testNetworkParameters(emptyList(), minimumPlatformVersion = 2))
}
// Create node info.
val (_, signedNodeInfo) = createNodeInfoAndSigned(CordaX500Name("Test", "London", "GB"), platformVersion = 1)
NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(mock(), networkMapStorage, testNetworkMapConfig)).use {
it.start()
val nodeInfoAndSignature = signedNodeInfo.serialize().bytes
assertThatThrownBy { it.doPost("publish", nodeInfoAndSignature) }
.hasMessageStartingWith("Response Code 400: Minimum platform version is 2")
}
}
@Test
fun `submit nodeInfo when no network parameters`() {
val networkMapStorage: NetworkMapStorage = mock {
on { getCurrentNetworkParameters() }.thenReturn(null)
}
// Create node info.
val (_, signedNodeInfo) = createNodeInfoAndSigned(CordaX500Name("Test", "London", "GB"), platformVersion = 1)
NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(mock(), networkMapStorage, testNetworkMapConfig)).use {
it.start()
val nodeInfoAndSignature = signedNodeInfo.serialize().bytes
assertThatThrownBy { it.doPost("publish", nodeInfoAndSignature) }
.hasMessageStartingWith("Response Code 503: Network parameters have not been initialised")
}
}
@Test
fun `get network map`() {
val networkMap = NetworkMap(listOf(randomSHA256(), randomSHA256()), randomSHA256())
@ -136,7 +173,10 @@ class NodeInfoWebServiceTest {
requestMethod = "POST"
setRequestProperty("Content-Type", MediaType.APPLICATION_OCTET_STREAM)
outputStream.write(payload)
inputStream.close() // This will give us a nice IOException if the response isn't HTTP 200
if (responseCode != 200) {
throw IOException("Response Code $responseCode: ${IOUtils.toString(errorStream, Charset.defaultCharset())}")
}
inputStream.close()
}
}

View File

@ -22,17 +22,17 @@ class TestNodeInfoBuilder {
}
}
fun build(serial: Long = 1): NodeInfo {
fun build(serial: Long = 1, platformVersion: Int = 1): NodeInfo {
return NodeInfo(
listOf(NetworkHostAndPort("my.${identitiesAndPrivateKeys[0].first.party.name.organisation}.com", 1234)),
identitiesAndPrivateKeys.map { it.first },
1,
platformVersion,
serial
)
}
fun buildWithSigned(serial: Long = 1): Pair<NodeInfo, SignedNodeInfo> {
val nodeInfo = build(serial)
fun buildWithSigned(serial: Long = 1, platformVersion: Int = 1): Pair<NodeInfo, SignedNodeInfo> {
val nodeInfo = build(serial, platformVersion)
val privateKeys = identitiesAndPrivateKeys.map { it.second }
return Pair(nodeInfo, nodeInfo.signWith(privateKeys))
}
@ -42,10 +42,10 @@ class TestNodeInfoBuilder {
}
}
fun createNodeInfoAndSigned(vararg names: CordaX500Name, serial: Long = 1): Pair<NodeInfo, SignedNodeInfo> {
fun createNodeInfoAndSigned(vararg names: CordaX500Name, serial: Long = 1, platformVersion: Int = 1): Pair<NodeInfo, SignedNodeInfo> {
val nodeInfoBuilder = TestNodeInfoBuilder()
names.forEach { nodeInfoBuilder.addIdentity(it) }
return nodeInfoBuilder.buildWithSigned(serial)
return nodeInfoBuilder.buildWithSigned(serial, platformVersion)
}
fun NodeInfo.signWith(keys: List<PrivateKey>): SignedNodeInfo {