mirror of
https://github.com/corda/corda.git
synced 2025-01-27 06:39:38 +00:00
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:
parent
2832eb489b
commit
a65db712c7
@ -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
|
||||
}
|
@ -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()
|
||||
}
|
@ -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())
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user