diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/NodeInfoStorage.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/NodeInfoStorage.kt index 3f44d27df0..38777f1adc 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/NodeInfoStorage.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/NodeInfoStorage.kt @@ -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 } \ No newline at end of file diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/NodeInfoWithSigned.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/NodeInfoWithSigned.kt new file mode 100644 index 0000000000..c5766e864e --- /dev/null +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/NodeInfoWithSigned.kt @@ -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() +} \ No newline at end of file diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNodeInfoStorage.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNodeInfoStorage.kt index bea2f44bce..5c8d249eac 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNodeInfoStorage.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNodeInfoStorage.kt @@ -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(CertificateDataEntity::publicKeyHash.name), it.publicKey.encoded.sha256().toString()) diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/NodeInfoWebService.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/NodeInfoWebService.kt index 0f4f41aacb..2ac26743e2 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/NodeInfoWebService.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/NodeInfoWebService.kt @@ -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 = CacheBuilder.newBuilder() + private val networkMapCache: LoadingCache> = 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() + val signedNodeInfo = input.readBytes().deserialize() 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) } diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNetworkMapStorageTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNetworkMapStorageTest.kt index de5ef7e27b..70bd9dc6e1 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNetworkMapStorageTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNetworkMapStorageTest.kt @@ -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) } } \ No newline at end of file diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersitenceNodeInfoStorageTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNodeInfoStorageTest.kt similarity index 79% rename from network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersitenceNodeInfoStorageTest.kt rename to network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNodeInfoStorageTest.kt index c8df5a3a3d..7fd0f9cdf7 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersitenceNodeInfoStorageTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNodeInfoStorageTest.kt @@ -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 { + private fun createValidSignedNodeInfo(organisation: String, serial: Long = 1): Pair { 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) } } \ No newline at end of file diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/NodeInfoWebServiceTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/NodeInfoWebServiceTest.kt index b376833aa1..b1f0e320db 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/NodeInfoWebServiceTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/NodeInfoWebServiceTest.kt @@ -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() } } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestNodeInfoBuilder.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestNodeInfoBuilder.kt index a367cff7e6..bdeba56fc6 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestNodeInfoBuilder.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestNodeInfoBuilder.kt @@ -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 { - val nodeInfo = build(serial) + fun buildWithSigned(serial: Long = 1, platformVersion: Int = 1): Pair { + 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 { +fun createNodeInfoAndSigned(vararg names: CordaX500Name, serial: Long = 1, platformVersion: Int = 1): Pair { val nodeInfoBuilder = TestNodeInfoBuilder() names.forEach { nodeInfoBuilder.addIdentity(it) } - return nodeInfoBuilder.buildWithSigned(serial) + return nodeInfoBuilder.buildWithSigned(serial, platformVersion) } fun NodeInfo.signWith(keys: List): SignedNodeInfo {