From 2dc73ecf3b748362d5a0a11baa521c05db49cbfc Mon Sep 17 00:00:00 2001 From: Alberto Arri <30873160+al-r3@users.noreply.github.com> Date: Tue, 12 Dec 2017 10:39:52 +0000 Subject: [PATCH] Changed logic to check for initial certificate, now using the truststore instead of the .cer file (#2213) --- .../internal/config/SSLConfiguration.kt | 2 - .../registration/NodeRegistrationTest.kt | 26 +++++++- .../registration/NetworkRegistrationHelper.kt | 55 +++++++--------- .../NetworkRegistrationHelperTest.kt | 66 ++++++++----------- .../net/corda/testing/driver/DriverTests.kt | 17 +++++ .../corda/testing/internal/DriverDSLImpl.kt | 10 ++- 6 files changed, 100 insertions(+), 76 deletions(-) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/SSLConfiguration.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/SSLConfiguration.kt index 590b89110d..90544e92a6 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/SSLConfiguration.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/SSLConfiguration.kt @@ -15,6 +15,4 @@ interface SSLConfiguration { interface NodeSSLConfiguration : SSLConfiguration { val baseDirectory: Path override val certificatesDirectory: Path get() = baseDirectory / "certificates" - // TODO This will be removed. Instead we will just check against the truststore, which will be provided out-of-band, along with its password - val rootCertFile: Path get() = certificatesDirectory / "rootcert.pem" } diff --git a/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt b/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt index 0301c84dd3..6cd0058792 100644 --- a/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt @@ -1,9 +1,11 @@ package net.corda.node.utilities.registration +import com.google.common.net.HostAndPort import net.corda.core.crypto.Crypto import net.corda.core.identity.CordaX500Name import net.corda.core.internal.cert import net.corda.core.internal.toX509CertHolder +import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.minutes import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair @@ -18,6 +20,7 @@ import net.corda.testing.driver.PortAllocation import net.corda.testing.internal.internalDriver import net.corda.testing.node.network.NetworkMapServer import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy import org.bouncycastle.pkcs.PKCS10CertificationRequest import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest import org.junit.After @@ -41,13 +44,12 @@ class NodeRegistrationTest { private val registrationHandler = RegistrationHandler(rootCertAndKeyPair) private lateinit var server: NetworkMapServer - private lateinit var compatibilityZone: CompatibilityZoneParams + private lateinit var serverHostAndPort: NetworkHostAndPort @Before fun startServer() { server = NetworkMapServer(1.minutes, portAllocation.nextHostAndPort(), registrationHandler) - val address = server.start() - compatibilityZone = CompatibilityZoneParams(URL("http://$address"), rootCert = rootCertAndKeyPair.certificate.cert) + serverHostAndPort = server.start() } @After @@ -59,6 +61,7 @@ class NodeRegistrationTest { // starting a second node hangs so that needs to be fixed. @Test fun `node registration correct root cert`() { + val compatibilityZone = CompatibilityZoneParams(URL("http://$serverHostAndPort"), rootCert = rootCertAndKeyPair.certificate.cert) internalDriver( portAllocation = portAllocation, notarySpecs = emptyList(), @@ -69,6 +72,23 @@ class NodeRegistrationTest { } } + @Test + fun `node registration wrong root cert`() { + val someCert = createSelfKeyAndSelfSignedCertificate().certificate.cert + val compatibilityZone = CompatibilityZoneParams(URL("http://$serverHostAndPort"), rootCert = someCert) + internalDriver( + portAllocation = portAllocation, + notarySpecs = emptyList(), + compatibilityZone = compatibilityZone, + // Changing the content of the truststore makes the node fail in a number of ways if started out process. + startNodesInProcess = true + ) { + assertThatThrownBy { + startNode(providedName = CordaX500Name("Alice", "London", "GB")).getOrThrow() + }.isInstanceOf(WrongRootCertException::class.java) + } + } + private fun createSelfKeyAndSelfSignedCertificate(): CertificateAndKeyPair { val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val rootCACert = X509Utilities.createSelfSignedCACertificate( diff --git a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt index 08808b066f..f24cf56f11 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt @@ -27,18 +27,27 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration, private v val SELF_SIGNED_PRIVATE_KEY = "Self Signed Private Key" } - init { - require(config.rootCertFile.exists()) { - "${config.rootCertFile} does not exist. This file must contain the root CA cert of your compatibility zone. " + - "Please contact your CZ operator." - } - } - private val requestIdStore = config.certificatesDirectory / "certificate-request-id.txt" private val keystorePassword = config.keyStorePassword // TODO: Use different password for private key. private val privateKeyPassword = config.keyStorePassword - private val rootCert = X509Utilities.loadCertificateFromPEMFile(config.rootCertFile) + private val trustStore: KeyStore + private val rootCert: Certificate + + init { + require(config.trustStoreFile.exists()) { + "${config.trustStoreFile} does not exist. This file must contain the root CA cert of your compatibility zone. " + + "Please contact your CZ operator." + } + trustStore = loadKeyStore(config.trustStoreFile, config.trustStorePassword) + val rootCert = trustStore.getCertificate(CORDA_ROOT_CA) + require(rootCert != null) { + "${config.trustStoreFile} does not contain a certificate with the key $CORDA_ROOT_CA." + + "This file must contain the root CA cert of your compatibility zone. " + + "Please contact your CZ operator." + } + this.rootCert = rootCert + } /** * Ensure the initial keystore for a node is set up. @@ -83,18 +92,14 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration, private v caKeyStore.addOrReplaceKey(CORDA_CLIENT_CA, keyPair.private, privateKeyPassword.toCharArray(), certificates) caKeyStore.deleteEntry(SELF_SIGNED_PRIVATE_KEY) caKeyStore.save(config.nodeKeystore, keystorePassword) - - // Check the root certificate. - val returnedRootCa = certificates.last() - checkReturnedRootCaMatchesExpectedCa(returnedRootCa) - - // Save root certificates to trust store. - val trustStore = loadOrCreateKeyStore(config.trustStoreFile, config.trustStorePassword) - // Assumes certificate chain always starts with client certificate and end with root certificate. - trustStore.addOrReplaceCertificate(CORDA_ROOT_CA, returnedRootCa) - trustStore.save(config.trustStoreFile, config.trustStorePassword) println("Node private key and certificate stored in ${config.nodeKeystore}.") + // Check that the root of the signed certificate matches the expected certificate in the truststore. + if (rootCert != certificates.last()) { + // Assumes certificate chain always starts with client certificate and end with root certificate. + throw WrongRootCertException(rootCert, certificates.last(), config.trustStoreFile) + } + println("Generating SSL certificate for node messaging service.") val sslKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val caCert = caKeyStore.getX509Certificate(CORDA_CLIENT_CA).toX509CertHolder() @@ -111,16 +116,6 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration, private v } } - /** - * Checks that the passed Certificate is the expected root CA. - * @throws WrongRootCertException if the certificates don't match. - */ - private fun checkReturnedRootCaMatchesExpectedCa(returnedRootCa: Certificate) { - if (rootCert != returnedRootCa) { - throw WrongRootCertException(rootCert, returnedRootCa, config.rootCertFile) - } - } - /** * Poll Certificate Signing Server for approved certificate, * enter a slow polling loop if server return null. @@ -177,7 +172,7 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration, private v /** * Exception thrown when the doorman root certificate doesn't match the expected (out-of-band) root certificate. - * This usually means the has been a Man-in-the-middle attack when contacting the doorman. + * This usually means that there has been a Man-in-the-middle attack when contacting the doorman. */ class WrongRootCertException(expected: Certificate, actual: Certificate, @@ -186,5 +181,5 @@ class WrongRootCertException(expected: Certificate, The Root CA returned back from the registration process does not match the expected Root CA expected: $expected actual: $actual - the expected certificate is stored in: $expectedFilePath + the expected certificate is stored in: $expectedFilePath with alias $CORDA_ROOT_CA """.trimMargin()) diff --git a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt index 97a9e2b818..8c76a0ad82 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt @@ -9,9 +9,8 @@ import net.corda.core.crypto.SecureHash import net.corda.core.identity.CordaX500Name import net.corda.core.internal.* import net.corda.node.services.config.NodeConfiguration -import net.corda.nodeapi.internal.crypto.X509Utilities -import net.corda.nodeapi.internal.crypto.getX509Certificate -import net.corda.nodeapi.internal.crypto.loadKeyStore +import net.corda.node.services.config.createKeystoreForCordaNode +import net.corda.nodeapi.internal.crypto.* import net.corda.testing.ALICE import net.corda.testing.rigorousMock import net.corda.testing.testNodeConfiguration @@ -33,6 +32,15 @@ class NetworkRegistrationHelperTest { private val requestId = SecureHash.randomSHA256().toString() private lateinit var config: NodeConfiguration + private val identities = listOf("CORDA_CLIENT_CA", + "CORDA_INTERMEDIATE_CA", + "CORDA_ROOT_CA") + .map { CordaX500Name(commonName = it, organisation = "R3 Ltd", locality = "London", country = "GB") } + private val certs = identities.map { X509Utilities.createSelfSignedCACertificate(it, Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)) } + .map { it.cert }.toTypedArray() + + private val certService = mockRegistrationResponse(*certs) + @Before fun init() { config = testNodeConfiguration(baseDirectory = tempFolder.root.toPath(), myLegalName = ALICE.name) @@ -40,21 +48,13 @@ class NetworkRegistrationHelperTest { @Test fun `successful registration`() { - val identities = listOf("CORDA_CLIENT_CA", - "CORDA_INTERMEDIATE_CA", - "CORDA_ROOT_CA") - .map { CordaX500Name(commonName = it, organisation = "R3 Ltd", locality = "London", country = "GB") } - val certs = identities.stream().map { X509Utilities.createSelfSignedCACertificate(it, Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)) } - .map { it.cert }.toTypedArray() - - val certService = mockRegistrationResponse(*certs) - - config.rootCertFile.parent.createDirectories() - X509Utilities.saveCertificateAsPEMFile(certs.last(), config.rootCertFile) - assertFalse(config.nodeKeystore.exists()) assertFalse(config.sslKeystore.exists()) - assertFalse(config.trustStoreFile.exists()) + config.trustStoreFile.parent.createDirectories() + loadOrCreateKeyStore(config.trustStoreFile, config.trustStorePassword).also { + it.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, certs.last()) + it.save(config.trustStoreFile, config.trustStorePassword) + } NetworkRegistrationHelper(config, certService).buildKeystore() @@ -97,33 +97,21 @@ class NetworkRegistrationHelperTest { } @Test - fun `rootCertFile doesn't exist`() { - val certService = rigorousMock() - + fun `missing truststore`() { assertThatThrownBy { - NetworkRegistrationHelper(config, certService) - }.hasMessageContaining(config.rootCertFile.toString()) + NetworkRegistrationHelper(config, certService).buildKeystore() + }.hasMessageContaining("This file must contain the root CA cert of your compatibility zone. Please contact your CZ operator.") + .isInstanceOf(IllegalArgumentException::class.java) } @Test - fun `root cert in response doesn't match expected`() { - val identities = listOf("CORDA_CLIENT_CA", - "CORDA_INTERMEDIATE_CA", - "CORDA_ROOT_CA") - .map { CordaX500Name(commonName = it, organisation = "R3 Ltd", locality = "London", country = "GB") } - val certs = identities.stream().map { X509Utilities.createSelfSignedCACertificate(it, Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)) } - .map { it.cert }.toTypedArray() - - val certService = mockRegistrationResponse(*certs) - - config.rootCertFile.parent.createDirectories() - X509Utilities.saveCertificateAsPEMFile( - X509Utilities.createSelfSignedCACertificate( - CordaX500Name("CORDA_ROOT_CA", "R3 Ltd", "London", "GB"), - Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)).cert, - config.rootCertFile - ) - + fun `wrong root cert in truststore`() { + val someCert = X509Utilities.createSelfSignedCACertificate(CordaX500Name("Foo", "MU", "GB"), Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)).cert + config.trustStoreFile.parent.createDirectories() + loadOrCreateKeyStore(config.trustStoreFile, config.trustStorePassword).also { + it.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, someCert) + it.save(config.trustStoreFile, config.trustStorePassword) + } assertThatThrownBy { NetworkRegistrationHelper(config, certService).buildKeystore() }.isInstanceOf(WrongRootCertException::class.java) diff --git a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt index 89f8076973..a429d8f204 100644 --- a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt +++ b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt @@ -1,28 +1,45 @@ package net.corda.testing.driver import net.corda.core.concurrent.CordaFuture +import net.corda.core.internal.copyTo import net.corda.core.internal.div import net.corda.core.internal.list import net.corda.core.internal.readLines import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.seconds import net.corda.node.internal.NodeStartup +import net.corda.nodeapi.internal.crypto.X509Utilities +import net.corda.nodeapi.internal.crypto.getX509Certificate +import net.corda.nodeapi.internal.crypto.loadOrCreateKeyStore import net.corda.testing.DUMMY_BANK_A import net.corda.testing.DUMMY_NOTARY import net.corda.testing.DUMMY_REGULATOR import net.corda.testing.common.internal.ProjectStructure.projectRootDir import net.corda.testing.http.HttpApi +import net.corda.testing.internal.CompatibilityZoneParams import net.corda.testing.internal.addressMustBeBound import net.corda.testing.internal.addressMustNotBeBound import net.corda.testing.internal.internalDriver import net.corda.testing.node.NotarySpec +import net.corda.testing.node.network.NetworkMapServer import org.assertj.core.api.Assertions.assertThat import org.json.simple.JSONObject +import org.junit.Rule import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.net.URL import java.util.concurrent.Executors import java.util.concurrent.ScheduledExecutorService +import javax.ws.rs.GET +import javax.ws.rs.POST +import javax.ws.rs.Path +import javax.ws.rs.core.Response +import javax.ws.rs.core.Response.ok + class DriverTests { + companion object { private val executorService: ScheduledExecutorService = Executors.newScheduledThreadPool(2) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/internal/DriverDSLImpl.kt index 8ecb8cef13..09ad8aa7ac 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/internal/DriverDSLImpl.kt @@ -39,6 +39,9 @@ import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.config.parseAs import net.corda.nodeapi.internal.config.toConfig import net.corda.nodeapi.internal.crypto.X509Utilities +import net.corda.nodeapi.internal.crypto.addOrReplaceCertificate +import net.corda.nodeapi.internal.crypto.loadOrCreateKeyStore +import net.corda.nodeapi.internal.crypto.save import net.corda.testing.ALICE import net.corda.testing.BOB import net.corda.testing.DUMMY_BANK_A @@ -220,8 +223,11 @@ class DriverDSLImpl( ) val configuration = config.parseAsNodeConfiguration() - configuration.rootCertFile.parent.createDirectories() - X509Utilities.saveCertificateAsPEMFile(rootCert, configuration.rootCertFile) + configuration.trustStoreFile.parent.createDirectories() + loadOrCreateKeyStore(configuration.trustStoreFile, configuration.trustStorePassword).also { + it.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCert) + it.save(configuration.trustStoreFile, configuration.trustStorePassword) + } return if (startNodesInProcess) { // This is a bit cheating, we're not starting a full node, we're just calling the code nodes call