ENT-1323 Network map service to check all identities in submitted node info (#499)

* ENT-1323 Network map service to check all identities in submitted node info

* fixup after rebase

* address PR issues, refactored createValidNodeInfo

* address PR issues
This commit is contained in:
Patrick Kuo 2018-04-12 13:05:15 +01:00 committed by GitHub
parent 241f7738e4
commit f9ed55b731
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 118 additions and 36 deletions

View File

@ -34,10 +34,11 @@ import java.security.cert.CertPath
class PersistentNodeInfoStorage(private val database: CordaPersistence) : NodeInfoStorage { class PersistentNodeInfoStorage(private val database: CordaPersistence) : NodeInfoStorage {
override fun putNodeInfo(nodeInfoAndSigned: NodeInfoAndSigned): SecureHash { override fun putNodeInfo(nodeInfoAndSigned: NodeInfoAndSigned): SecureHash {
val (nodeInfo, signedNodeInfo) = nodeInfoAndSigned val (nodeInfo, signedNodeInfo) = nodeInfoAndSigned
val nodeCaCert = nodeInfo.legalIdentitiesAndCerts[0].certPath.x509Certificates.find { CertRole.extract(it) == NODE_CA }
nodeCaCert ?: throw IllegalArgumentException("Missing Node CA")
val nodeInfoHash = signedNodeInfo.raw.hash val nodeInfoHash = signedNodeInfo.raw.hash
// Extract identities issued by the intermediate CAs (doorman).
val registeredIdentities = nodeInfo.legalIdentitiesAndCerts.map { it.certPath.x509Certificates.single { CertRole.extract(it) in setOf(CertRole.SERVICE_IDENTITY, NODE_CA) } }
database.transaction { database.transaction {
val count = session.createQuery( val count = session.createQuery(
"select count(*) from ${NodeInfoEntity::class.java.name} where nodeInfoHash = :nodeInfoHash and isCurrent = true", java.lang.Long::class.java) "select count(*) from ${NodeInfoEntity::class.java.name} where nodeInfoHash = :nodeInfoHash and isCurrent = true", java.lang.Long::class.java)
@ -50,13 +51,21 @@ class PersistentNodeInfoStorage(private val database: CordaPersistence) : NodeIn
} }
// TODO Move these checks out of data access layer // TODO Move these checks out of data access layer
val request = requireNotNull(getSignedRequestByPublicHash(nodeCaCert.publicKey.encoded.sha256())) { // For each identity known by the doorman, validate against it's CSR.
"Node-info not registered with us" val requests = registeredIdentities.map {
} val request = requireNotNull(getSignedRequestByPublicHash(it.publicKey.hash)) {
request.certificateData?.certificateStatus.let { "Node-info not registered with us"
require(it == CertificateStatus.VALID) { "Certificate is no longer valid: $it" } }
request.certificateData?.certificateStatus.let {
require(it == CertificateStatus.VALID) { "Certificate is no longer valid: $it" }
}
CertRole.extract(it) to request
} }
// Ensure only 1 NodeCA identity.
// TODO: make this support multiple node identities.
val (_, request) = requireNotNull(requests.singleOrNull { it.first == CertRole.NODE_CA }) { "Require exactly 1 Node CA identity in the node-info." }
val existingNodeInfos = session.fromQuery<NodeInfoEntity>( val existingNodeInfos = session.fromQuery<NodeInfoEntity>(
"n where n.certificateSigningRequest = :csr and n.isCurrent = true order by n.publishedAt desc") "n where n.certificateSigningRequest = :csr and n.isCurrent = true order by n.publishedAt desc")
.setParameter("csr", request) .setParameter("csr", request)

View File

@ -13,10 +13,10 @@ package com.r3.corda.networkmanage.common.persistence
import com.r3.corda.networkmanage.TestBase import com.r3.corda.networkmanage.TestBase
import com.r3.corda.networkmanage.common.persistence.entity.NodeInfoEntity import com.r3.corda.networkmanage.common.persistence.entity.NodeInfoEntity
import net.corda.core.crypto.Crypto import net.corda.core.crypto.Crypto
import net.corda.core.crypto.sha256
import net.corda.core.identity.CordaX500Name import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.CertRole import net.corda.core.internal.CertRole
import net.corda.core.internal.hash import net.corda.core.internal.hash
import net.corda.core.node.NodeInfo
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.corda.core.utilities.days import net.corda.core.utilities.days
import net.corda.nodeapi.internal.NodeInfoAndSigned import net.corda.nodeapi.internal.NodeInfoAndSigned
@ -31,6 +31,7 @@ import net.corda.testing.internal.createDevIntermediateCaCertPath
import net.corda.testing.internal.signWith import net.corda.testing.internal.signWith
import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
@ -216,17 +217,66 @@ class PersistentNodeInfoStorageTest : TestBase() {
val acceptedUpdate = nodeInfoStorage.getAcceptedParametersUpdate(nodeInfoHash2) val acceptedUpdate = nodeInfoStorage.getAcceptedParametersUpdate(nodeInfoHash2)
assertThat(acceptedUpdate?.networkParameters?.hash).isEqualTo(netParamsHash.toString()) assertThat(acceptedUpdate?.networkParameters?.hash).isEqualTo(netParamsHash.toString())
} }
@Test
fun `persist node info with multiple node CA identities`() {
val (nodeInfo1, nodeKeyPair1) = createValidNodeInfo("Alice", requestStorage)
val (nodeInfo2, nodeKeyPair2) = createValidNodeInfo("Bob", requestStorage)
val multiIdentityNodeInfo = nodeInfo1.copy(legalIdentitiesAndCerts = nodeInfo1.legalIdentitiesAndCerts + nodeInfo2.legalIdentitiesAndCerts)
val signedNodeInfo = multiIdentityNodeInfo.signWith(listOf(nodeKeyPair1, nodeKeyPair2))
assertThatThrownBy { nodeInfoStorage.putNodeInfo(NodeInfoAndSigned(signedNodeInfo)) }
.isInstanceOf(IllegalArgumentException::class.java)
.hasMessageContaining("Require exactly 1 Node CA identity in the node-info.")
}
@Test
fun `persist node info with service identity`() {
val (nodeInfo, nodeKeyPairs) = createValidNodeInfo(requestStorage, CertRole.NODE_CA to "Alice", CertRole.SERVICE_IDENTITY to "Alice Notary")
val signedNodeInfo = nodeInfo.signWith(nodeKeyPairs)
nodeInfoStorage.putNodeInfo(NodeInfoAndSigned(signedNodeInfo))
}
@Test
fun `persist node info with unregistered service identity`() {
val (nodeInfo1, nodeKeyPair1) = createValidNodeInfo("Alice", requestStorage)
// Create a unregistered cert path with valid intermediate cert.
val (identity, key) = TestNodeInfoBuilder().addServiceIdentity(CordaX500Name("Test", "London", "GB"), Crypto.generateKeyPair())
val multiIdentityNodeInfo = nodeInfo1.copy(legalIdentitiesAndCerts = nodeInfo1.legalIdentitiesAndCerts + identity)
val signedNodeInfo = multiIdentityNodeInfo.signWith(listOf(nodeKeyPair1, key))
assertThatThrownBy { nodeInfoStorage.putNodeInfo(NodeInfoAndSigned(signedNodeInfo)) }
.isInstanceOf(IllegalArgumentException::class.java)
.hasMessageContaining("Node-info not registered with us")
}
} }
internal fun createValidSignedNodeInfo(organisation: String, private fun createValidNodeInfo(organisation: String, storage: CertificateSigningRequestStorage): Pair<NodeInfo, PrivateKey> {
storage: CertificateSigningRequestStorage): Pair<NodeInfoAndSigned, PrivateKey> { val (nodeInfo, keys) = createValidNodeInfo(storage, CertRole.NODE_CA to organisation)
val (csr, nodeKeyPair) = createRequest(organisation, certRole = CertRole.NODE_CA) return Pair(nodeInfo, keys.single())
val requestId = storage.saveRequest(csr) }
storage.markRequestTicketCreated(requestId)
storage.approveRequest(requestId, "TestUser") private fun createValidNodeInfo(storage: CertificateSigningRequestStorage, vararg identities: Pair<CertRole, String>): Pair<NodeInfo, List<PrivateKey>> {
val nodeInfoBuilder = TestNodeInfoBuilder() val nodeInfoBuilder = TestNodeInfoBuilder()
val (identity, key) = nodeInfoBuilder.addIdentity(CordaX500Name.build(X500Principal(csr.subject.encoded)), nodeKeyPair) val keys = identities.map { (certRole, name) ->
storage.putCertificatePath(requestId, identity.certPath, "Test") val (csr, nodeKeyPair) = createRequest(name, certRole = certRole)
val (_, signedNodeInfo) = nodeInfoBuilder.buildWithSigned(1) val requestId = storage.saveRequest(csr)
return Pair(NodeInfoAndSigned(signedNodeInfo), key) storage.markRequestTicketCreated(requestId)
} storage.approveRequest(requestId, "TestUser")
val (identity, key) = when (certRole) {
CertRole.NODE_CA -> nodeInfoBuilder.addLegalIdentity(CordaX500Name.build(X500Principal(csr.subject.encoded)), nodeKeyPair)
CertRole.SERVICE_IDENTITY -> nodeInfoBuilder.addServiceIdentity(CordaX500Name.build(X500Principal(csr.subject.encoded)), nodeKeyPair)
else -> throw IllegalArgumentException("Unsupported cert role $certRole.")
}
storage.putCertificatePath(requestId, identity.certPath, "Test")
key
}
return Pair(nodeInfoBuilder.build(), keys)
}
internal fun createValidSignedNodeInfo(organisation: String, storage: CertificateSigningRequestStorage): Pair<NodeInfoAndSigned, PrivateKey> {
val (nodeInfo, key) = createValidNodeInfo(organisation, storage)
return Pair(NodeInfoAndSigned(nodeInfo.signWith(listOf(key))), key)
}

View File

@ -41,23 +41,23 @@ class SignedNodeInfoTest {
@Test @Test
fun `verifying single identity`() { fun `verifying single identity`() {
nodeInfoBuilder.addIdentity(ALICE_NAME) nodeInfoBuilder.addLegalIdentity(ALICE_NAME)
val (nodeInfo, signedNodeInfo) = nodeInfoBuilder.buildWithSigned() val (nodeInfo, signedNodeInfo) = nodeInfoBuilder.buildWithSigned()
assertThat(signedNodeInfo.verified()).isEqualTo(nodeInfo) assertThat(signedNodeInfo.verified()).isEqualTo(nodeInfo)
} }
@Test @Test
fun `verifying multiple identities`() { fun `verifying multiple identities`() {
nodeInfoBuilder.addIdentity(ALICE_NAME) nodeInfoBuilder.addLegalIdentity(ALICE_NAME)
nodeInfoBuilder.addIdentity(BOB_NAME) nodeInfoBuilder.addLegalIdentity(BOB_NAME)
val (nodeInfo, signedNodeInfo) = nodeInfoBuilder.buildWithSigned() val (nodeInfo, signedNodeInfo) = nodeInfoBuilder.buildWithSigned()
assertThat(signedNodeInfo.verified()).isEqualTo(nodeInfo) assertThat(signedNodeInfo.verified()).isEqualTo(nodeInfo)
} }
@Test @Test
fun `verifying missing signature`() { fun `verifying missing signature`() {
val (_, aliceKey) = nodeInfoBuilder.addIdentity(ALICE_NAME) val (_, aliceKey) = nodeInfoBuilder.addLegalIdentity(ALICE_NAME)
nodeInfoBuilder.addIdentity(BOB_NAME) nodeInfoBuilder.addLegalIdentity(BOB_NAME)
val nodeInfo = nodeInfoBuilder.build() val nodeInfo = nodeInfoBuilder.build()
val signedNodeInfo = nodeInfo.signWith(listOf(aliceKey)) val signedNodeInfo = nodeInfo.signWith(listOf(aliceKey))
assertThatThrownBy { signedNodeInfo.verified() } assertThatThrownBy { signedNodeInfo.verified() }
@ -80,7 +80,7 @@ class SignedNodeInfoTest {
@Test @Test
fun `verifying extra signature`() { fun `verifying extra signature`() {
val (_, aliceKey) = nodeInfoBuilder.addIdentity(ALICE_NAME) val (_, aliceKey) = nodeInfoBuilder.addLegalIdentity(ALICE_NAME)
val nodeInfo = nodeInfoBuilder.build() val nodeInfo = nodeInfoBuilder.build()
val signedNodeInfo = nodeInfo.signWith(listOf(aliceKey, generateKeyPair().private)) val signedNodeInfo = nodeInfo.signWith(listOf(aliceKey, generateKeyPair().private))
assertThatThrownBy { signedNodeInfo.verified() } assertThatThrownBy { signedNodeInfo.verified() }
@ -90,7 +90,7 @@ class SignedNodeInfoTest {
@Test @Test
fun `verifying incorrect signature`() { fun `verifying incorrect signature`() {
nodeInfoBuilder.addIdentity(ALICE_NAME) nodeInfoBuilder.addLegalIdentity(ALICE_NAME)
val nodeInfo = nodeInfoBuilder.build() val nodeInfo = nodeInfoBuilder.build()
val signedNodeInfo = nodeInfo.signWith(listOf(generateKeyPair().private)) val signedNodeInfo = nodeInfo.signWith(listOf(generateKeyPair().private))
assertThatThrownBy { signedNodeInfo.verified() } assertThatThrownBy { signedNodeInfo.verified() }
@ -100,8 +100,8 @@ class SignedNodeInfoTest {
@Test @Test
fun `verifying with signatures in wrong order`() { fun `verifying with signatures in wrong order`() {
val (_, aliceKey) = nodeInfoBuilder.addIdentity(ALICE_NAME) val (_, aliceKey) = nodeInfoBuilder.addLegalIdentity(ALICE_NAME)
val (_, bobKey) = nodeInfoBuilder.addIdentity(BOB_NAME) val (_, bobKey) = nodeInfoBuilder.addLegalIdentity(BOB_NAME)
val nodeInfo = nodeInfoBuilder.build() val nodeInfo = nodeInfoBuilder.build()
val signedNodeInfo = nodeInfo.signWith(listOf(bobKey, aliceKey)) val signedNodeInfo = nodeInfo.signWith(listOf(bobKey, aliceKey))
assertThatThrownBy { signedNodeInfo.verified() } assertThatThrownBy { signedNodeInfo.verified() }

View File

@ -83,8 +83,8 @@ class NetworkMapClientTest {
@Test @Test
fun `errors return a meaningful error message`() { fun `errors return a meaningful error message`() {
val nodeInfoBuilder = TestNodeInfoBuilder() val nodeInfoBuilder = TestNodeInfoBuilder()
val (_, aliceKey) = nodeInfoBuilder.addIdentity(ALICE_NAME) val (_, aliceKey) = nodeInfoBuilder.addLegalIdentity(ALICE_NAME)
nodeInfoBuilder.addIdentity(BOB_NAME) nodeInfoBuilder.addLegalIdentity(BOB_NAME)
val nodeInfo3 = nodeInfoBuilder.build() val nodeInfo3 = nodeInfoBuilder.build()
val signedNodeInfo3 = nodeInfo3.signWith(listOf(aliceKey)) val signedNodeInfo3 = nodeInfo3.signWith(listOf(aliceKey))

View File

@ -30,7 +30,7 @@ import java.security.cert.X509Certificate
class TestNodeInfoBuilder(private val intermediateAndRoot: Pair<CertificateAndKeyPair, X509Certificate> = DEV_INTERMEDIATE_CA to DEV_ROOT_CA.certificate) { class TestNodeInfoBuilder(private val intermediateAndRoot: Pair<CertificateAndKeyPair, X509Certificate> = DEV_INTERMEDIATE_CA to DEV_ROOT_CA.certificate) {
private val identitiesAndPrivateKeys = ArrayList<Pair<PartyAndCertificate, PrivateKey>>() private val identitiesAndPrivateKeys = ArrayList<Pair<PartyAndCertificate, PrivateKey>>()
fun addIdentity(name: CordaX500Name, nodeKeyPair: KeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)): Pair<PartyAndCertificate, PrivateKey> { fun addLegalIdentity(name: CordaX500Name, nodeKeyPair: KeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)): Pair<PartyAndCertificate, PrivateKey> {
val nodeCertificateAndKeyPair = createDevNodeCa(intermediateAndRoot.first, name, nodeKeyPair) val nodeCertificateAndKeyPair = createDevNodeCa(intermediateAndRoot.first, name, nodeKeyPair)
val identityKeyPair = Crypto.generateKeyPair() val identityKeyPair = Crypto.generateKeyPair()
val identityCert = X509Utilities.createCertificate( val identityCert = X509Utilities.createCertificate(
@ -39,12 +39,35 @@ class TestNodeInfoBuilder(private val intermediateAndRoot: Pair<CertificateAndKe
nodeCertificateAndKeyPair.keyPair, nodeCertificateAndKeyPair.keyPair,
nodeCertificateAndKeyPair.certificate.subjectX500Principal, nodeCertificateAndKeyPair.certificate.subjectX500Principal,
identityKeyPair.public) identityKeyPair.public)
val certPath = X509Utilities.buildCertPath(
identityCert, val certs = arrayOf(identityCert, nodeCertificateAndKeyPair.certificate)
nodeCertificateAndKeyPair.certificate, val key = identityKeyPair.private
val certPath = X509Utilities.buildCertPath(*certs,
intermediateAndRoot.first.certificate, intermediateAndRoot.first.certificate,
intermediateAndRoot.second) intermediateAndRoot.second)
return Pair(PartyAndCertificate(certPath), identityKeyPair.private).also {
return Pair(PartyAndCertificate(certPath), key).also {
identitiesAndPrivateKeys += it
}
}
fun addServiceIdentity(name: CordaX500Name, nodeKeyPair: KeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)): Pair<PartyAndCertificate, PrivateKey> {
val serviceCert = X509Utilities.createCertificate(
CertificateType.SERVICE_IDENTITY,
intermediateAndRoot.first.certificate,
intermediateAndRoot.first.keyPair,
name.x500Principal,
nodeKeyPair.public)
val certs = arrayOf(serviceCert)
val key = nodeKeyPair.private
val certPath = X509Utilities.buildCertPath(*certs,
intermediateAndRoot.first.certificate,
intermediateAndRoot.second)
return Pair(PartyAndCertificate(certPath), key).also {
identitiesAndPrivateKeys += it identitiesAndPrivateKeys += it
} }
} }
@ -72,7 +95,7 @@ class TestNodeInfoBuilder(private val intermediateAndRoot: Pair<CertificateAndKe
fun createNodeInfoAndSigned(vararg names: CordaX500Name, serial: Long = 1, platformVersion: Int = 1): NodeInfoAndSigned { fun createNodeInfoAndSigned(vararg names: CordaX500Name, serial: Long = 1, platformVersion: Int = 1): NodeInfoAndSigned {
val nodeInfoBuilder = TestNodeInfoBuilder() val nodeInfoBuilder = TestNodeInfoBuilder()
names.forEach { nodeInfoBuilder.addIdentity(it) } names.forEach { nodeInfoBuilder.addLegalIdentity(it) }
return nodeInfoBuilder.buildWithSigned(serial, platformVersion) return nodeInfoBuilder.buildWithSigned(serial, platformVersion)
} }