From acd2281b2098b9a853cb57eae17137867a922ca0 Mon Sep 17 00:00:00 2001 From: Alberto Arri <30873160+al-r3@users.noreply.github.com> Date: Mon, 4 Dec 2017 12:53:22 +0000 Subject: [PATCH] ENT-1125 bootstrap root certificate (#2151) * ENT-1125 make nodes check that the returned signed certificate from Doorman has the expected root --- .../corda/nodeapi/config/SSLConfiguration.kt | 1 + .../internal/ServiceIdentityGenerator.kt | 13 +- .../nodeapi/internal/crypto/X509Utilities.kt | 5 + .../NetworkRegistrationHelperDriverTest.kt | 182 ++++++++++++++++++ .../registration/NetworkRegistrationHelper.kt | 33 +++- .../NetworkisRegistrationHelperTest.kt | 3 + .../net/corda/testing/driver/DriverTests.kt | 43 +---- .../kotlin/net/corda/testing/NodeTestUtils.kt | 1 + .../kotlin/net/corda/testing/driver/Driver.kt | 33 +++- .../net/corda/testing/internal/RPCDriver.kt | 5 +- .../net/corda/verifier/VerifierDriver.kt | 5 +- 11 files changed, 271 insertions(+), 53 deletions(-) create mode 100644 node/src/integration-test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperDriverTest.kt diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/config/SSLConfiguration.kt b/node-api/src/main/kotlin/net/corda/nodeapi/config/SSLConfiguration.kt index 6fb82e508f..a357bf7093 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/config/SSLConfiguration.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/config/SSLConfiguration.kt @@ -15,4 +15,5 @@ interface SSLConfiguration { interface NodeSSLConfiguration : SSLConfiguration { val baseDirectory: Path override val certificatesDirectory: Path get() = baseDirectory / "certificates" + val rootCaCertFile: Path get() = certificatesDirectory / "rootcacert.cer" } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ServiceIdentityGenerator.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ServiceIdentityGenerator.kt index 21cba120a2..44d420d893 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ServiceIdentityGenerator.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ServiceIdentityGenerator.kt @@ -9,8 +9,10 @@ import net.corda.core.internal.createDirectories import net.corda.core.internal.div import net.corda.core.utilities.trace import net.corda.nodeapi.internal.crypto.* +import org.bouncycastle.cert.X509CertificateHolder import org.slf4j.LoggerFactory import java.nio.file.Path +import java.security.cert.Certificate object ServiceIdentityGenerator { private val log = LoggerFactory.getLogger(javaClass) @@ -22,18 +24,25 @@ object ServiceIdentityGenerator { * @param dirs List of node directories to place the generated identity and key pairs in. * @param serviceName The legal name of the distributed service. * @param threshold The threshold for the generated group [CompositeKey]. + * @param rootCertertificate the certificate to use a Corda root CA. If not specified the one in + * net/corda/node/internal/certificates/cordadevcakeys.jks is used. */ fun generateToDisk(dirs: List, serviceName: CordaX500Name, serviceId: String, - threshold: Int = 1): Party { + threshold: Int = 1, + rootCertertificate: X509CertificateHolder? = null): Party { log.trace { "Generating a group identity \"serviceName\" for nodes: ${dirs.joinToString()}" } val keyPairs = (1..dirs.size).map { generateKeyPair() } val notaryKey = CompositeKey.Builder().addKeys(keyPairs.map { it.public }).build(threshold) val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/cordadevcakeys.jks"), "cordacadevpass") val issuer = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, "cordacadevkeypass") - val rootCert = caKeyStore.getCertificate(X509Utilities.CORDA_ROOT_CA) + val rootCert: Certificate = if (rootCertertificate != null) { + rootCertertificate.cert + } else { + caKeyStore.getCertificate(X509Utilities.CORDA_ROOT_CA) + } keyPairs.zip(dirs) { keyPair, dir -> val serviceKeyCert = X509Utilities.createCertificate(CertificateType.CLIENT_CA, issuer.certificate, issuer.keyPair, serviceName, keyPair.public) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt index def4ee9879..dfb6d17e81 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt @@ -313,6 +313,11 @@ class X509CertificateFactory { fun generateCertificate(input: InputStream): X509Certificate { return delegate.generateCertificate(input) as X509Certificate } + + // TODO migrate calls to [CertificateFactory#generateCertPath] to call this instead. + fun generateCertPath(vararg certificates: Certificate): CertPath { + return delegate.generateCertPath(certificates.asList()) + } } enum class CertificateType(val keyUsage: KeyUsage, vararg val purposes: KeyPurposeId, val isCA: Boolean) { diff --git a/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperDriverTest.kt b/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperDriverTest.kt new file mode 100644 index 0000000000..fb5a59fe14 --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperDriverTest.kt @@ -0,0 +1,182 @@ +package net.corda.node.utilities.registration + +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.contextLogger +import net.corda.core.utilities.minutes +import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair +import net.corda.nodeapi.internal.crypto.CertificateType +import net.corda.nodeapi.internal.crypto.X509CertificateFactory +import net.corda.nodeapi.internal.crypto.X509Utilities +import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_CA +import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_INTERMEDIATE_CA +import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA +import net.corda.testing.ALICE_NAME +import net.corda.testing.driver.PortAllocation +import net.corda.testing.driver.driver +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 +import org.junit.Before +import org.junit.Test +import java.io.ByteArrayOutputStream +import java.io.InputStream +import java.net.URL +import java.security.KeyPair +import java.security.cert.CertPath +import java.security.cert.Certificate +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream +import javax.ws.rs.* +import javax.ws.rs.core.MediaType +import javax.ws.rs.core.Response + +private const val REQUEST_ID = "requestId" + +private val x509CertificateFactory = X509CertificateFactory() +private val portAllocation = PortAllocation.Incremental(13000) + +/** + * Driver based tests for [NetworkRegistrationHelper] + */ +class NetworkRegistrationHelperDriverTest { + val rootCertAndKeyPair = createSelfKeyAndSelfSignedCertificate() + val rootCert = rootCertAndKeyPair.certificate + val handler = RegistrationHandler(rootCertAndKeyPair) + lateinit var server: NetworkMapServer + lateinit var host: String + var port: Int = 0 + val compatibilityZoneUrl get() = URL("http", host, port, "") + + @Before + fun startServer() { + server = NetworkMapServer(1.minutes, portAllocation.nextHostAndPort(), handler) + val (host, port) = server.start() + this.host = host + this.port = port + } + + @After + fun stopServer() { + server.close() + } + + @Test + fun `node registration correct root cert`() { + driver(portAllocation = portAllocation, + compatibilityZoneURL = compatibilityZoneUrl, + startNodesInProcess = true, + rootCertificate = rootCert + ) { + startNode(providedName = ALICE_NAME, initialRegistration = true).get() + } + + // We're getting: + // a request to sign the certificate then + // at least one poll request to see if the request has been approved. + // all the network map registration and download. + assertThat(handler.requests).startsWith("/certificate", "/certificate/" + REQUEST_ID) + } + + @Test + fun `node registration without root cert`() { + driver(portAllocation = portAllocation, + compatibilityZoneURL = compatibilityZoneUrl, + startNodesInProcess = true + ) { + assertThatThrownBy { + startNode(providedName = ALICE_NAME, initialRegistration = true).get() + }.isInstanceOf(java.nio.file.NoSuchFileException::class.java) + } + } + + @Test + fun `node registration wrong root cert`() { + driver(portAllocation = portAllocation, + compatibilityZoneURL = compatibilityZoneUrl, + startNodesInProcess = true, + rootCertificate = createSelfKeyAndSelfSignedCertificate().certificate + ) { + assertThatThrownBy { + startNode(providedName = ALICE_NAME, initialRegistration = true).get() + }.isInstanceOf(WrongRootCaCertificateException::class.java) + } + } +} + + +/** + * Simple registration handler which can handle a single request, which will be given request id [REQUEST_ID]. + */ +@Path("certificate") +class RegistrationHandler(private val certificateAndKeyPair: CertificateAndKeyPair) { + val requests = mutableListOf() + lateinit var certificationRequest: JcaPKCS10CertificationRequest + + @POST + @Consumes(MediaType.APPLICATION_OCTET_STREAM) + @Produces(MediaType.TEXT_PLAIN) + fun registration(input: InputStream): Response { + requests += "/certificate" + certificationRequest = input.use { JcaPKCS10CertificationRequest(it.readBytes()) } + return Response.ok(REQUEST_ID).build() + } + + @GET + @Path(REQUEST_ID) + fun reply(): Response { + requests += "/certificate/" + REQUEST_ID + val certPath = createSignedClientCertificate(certificationRequest, + certificateAndKeyPair.keyPair, arrayOf(certificateAndKeyPair.certificate.cert)) + return buildDoormanReply(certPath.certificates.toTypedArray()) + } +} + +// TODO this logic is shared with doorman itself, refactor this to be somewhere where both doorman and these tests +// can depend on +private fun createSignedClientCertificate(certificationRequest: PKCS10CertificationRequest, + caKeyPair: KeyPair, + caCertPath: Array): CertPath { + val request = JcaPKCS10CertificationRequest(certificationRequest) + val x509CertificateHolder = X509Utilities.createCertificate(CertificateType.CLIENT_CA, + caCertPath.first().toX509CertHolder(), + caKeyPair, + CordaX500Name.parse(request.subject.toString()).copy(commonName = X509Utilities.CORDA_CLIENT_CA_CN), + request.publicKey, + nameConstraints = null) + return x509CertificateFactory.generateCertPath(x509CertificateHolder.cert, *caCertPath) +} + +// TODO this logic is shared with doorman itself, refactor this to be somewhere where both doorman and these tests +// can depend on +private fun buildDoormanReply(certificates: Array): Response { + // Write certificate chain to a zip stream and extract the bit array output. + val baos = ByteArrayOutputStream() + ZipOutputStream(baos).use { zip -> + // Client certificate must come first and root certificate should come last. + listOf(CORDA_CLIENT_CA, CORDA_INTERMEDIATE_CA, CORDA_ROOT_CA).zip(certificates).forEach { + zip.putNextEntry(ZipEntry("${it.first}.cer")) + zip.write(it.second.encoded) + zip.closeEntry() + } + } + return Response.ok(baos.toByteArray()) + .type("application/zip") + .header("Content-Disposition", "attachment; filename=\"certificates.zip\"").build() +} + +private fun createSelfKeyAndSelfSignedCertificate(): CertificateAndKeyPair { + val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val rootCACert = X509Utilities.createSelfSignedCACertificate( + CordaX500Name(commonName = "Integration Test Corda Node Root CA", + organisation = "R3 Ltd", locality = "London", + country = "GB"), rootCAKey) + return CertificateAndKeyPair(rootCACert, rootCAKey) +} 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 c350c3dd97..83edb78fbe 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 @@ -12,6 +12,7 @@ import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA import org.bouncycastle.openssl.jcajce.JcaPEMWriter import org.bouncycastle.util.io.pem.PemObject import java.io.StringWriter +import java.nio.file.Path import java.security.KeyPair import java.security.KeyStore import java.security.cert.Certificate @@ -75,10 +76,15 @@ 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, certificates.last()) + trustStore.addOrReplaceCertificate(CORDA_ROOT_CA, returnedRootCa) trustStore.save(config.trustStoreFile, config.trustStorePassword) println("Node private key and certificate stored in ${config.nodeKeystore}.") @@ -98,6 +104,17 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration, private v } } + /** + * Checks that the passed Certificate is the expected root CA. + * @throws WrongRootCaCertificateException if the certificates don't match. + */ + private fun checkReturnedRootCaMatchesExpectedCa(returnedRootCa: Certificate) { + val expected = X509Utilities.loadCertificateFromPEMFile(config.rootCaCertFile).cert + if (expected != returnedRootCa) { + throw WrongRootCaCertificateException(expected, returnedRootCa, config.rootCaCertFile) + } + } + /** * Poll Certificate Signing Server for approved certificate, * enter a slow polling loop if server return null. @@ -151,3 +168,17 @@ 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. + */ +class WrongRootCaCertificateException(expected: Certificate, + actual: Certificate, + expectedFilePath: Path): + Exception(""" + 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 + """.trimMargin()) diff --git a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkisRegistrationHelperTest.kt b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkisRegistrationHelperTest.kt index 65745b7f3d..330b32fbc3 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkisRegistrationHelperTest.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkisRegistrationHelperTest.kt @@ -46,6 +46,9 @@ class NetworkRegistrationHelperTest { baseDirectory = tempFolder.root.toPath(), myLegalName = ALICE.name) + config.rootCaCertFile.parent.createDirectories() + X509Utilities.saveCertificateAsPEMFile(certs.last().toX509CertHolder(), config.rootCaCertFile) + assertFalse(config.nodeKeystore.exists()) assertFalse(config.sslKeystore.exists()) assertFalse(config.trustStoreFile.exists()) 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 108d53c719..e9fa30cffe 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 @@ -5,25 +5,17 @@ import net.corda.core.internal.div import net.corda.core.internal.list import net.corda.core.internal.readLines import net.corda.core.utilities.getOrThrow -import net.corda.core.utilities.minutes -import net.corda.core.utilities.seconds import net.corda.node.internal.NodeStartup 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.node.NotarySpec -import net.corda.testing.node.network.NetworkMapServer import org.assertj.core.api.Assertions.assertThat import org.junit.Test -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 { @@ -61,22 +53,6 @@ class DriverTests { nodeMustBeDown(nodeHandle) } - @Test - fun `node registration`() { - val handler = RegistrationHandler() - NetworkMapServer(1.seconds, portAllocation.nextHostAndPort(), handler).use { - val (host, port) = it.start() - driver(portAllocation = portAllocation, compatibilityZoneURL = URL("http://$host:$port")) { - // Wait for the node to have started. - startNode(initialRegistration = true).get() - } - } - // We're getting: - // a request to sign the certificate then - // at least one poll request to see if the request has been approved. - // all the network map registration and download. - assertThat(handler.requests).startsWith("/certificate", "/certificate/reply") - } @Test fun `debug mode enables debug logging level`() { @@ -106,20 +82,3 @@ class DriverTests { assertThat(baseDirectory / "process-id").doesNotExist() } } - -@Path("certificate") -class RegistrationHandler { - val requests = mutableListOf() - @POST - fun registration(): Response { - requests += "/certificate" - return ok("reply").build() - } - - @GET - @Path("reply") - fun reply(): Response { - requests += "/certificate/reply" - return ok().build() - } -} diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/NodeTestUtils.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/NodeTestUtils.kt index 09f1e7918a..9271b57bd1 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/NodeTestUtils.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/NodeTestUtils.kt @@ -78,6 +78,7 @@ fun testNodeConfiguration( doCallRealMethod().whenever(it).trustStoreFile doCallRealMethod().whenever(it).sslKeystore doCallRealMethod().whenever(it).nodeKeystore + doCallRealMethod().whenever(it).rootCaCertFile } } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt index d05f0f05b8..12851cdaf5 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt @@ -41,6 +41,7 @@ import net.corda.nodeapi.config.parseAs import net.corda.nodeapi.config.toConfig import net.corda.nodeapi.internal.NotaryInfo import net.corda.nodeapi.internal.addShutdownHook +import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.testing.* import net.corda.nodeapi.internal.NetworkParametersCopier import net.corda.testing.common.internal.testNetworkParameters @@ -51,6 +52,7 @@ import net.corda.testing.node.MockServices.Companion.MOCK_VERSION_INFO import net.corda.testing.node.NotarySpec import okhttp3.OkHttpClient import okhttp3.Request +import org.bouncycastle.cert.X509CertificateHolder import org.slf4j.Logger import rx.Observable import rx.observables.ConnectableObservable @@ -343,9 +345,11 @@ data class NodeParameters( * available from [DriverDSLExposedInterface.notaryHandles]. Defaults to a simple validating notary. * @param compatibilityZoneURL if not null each node is started once in registration mode (which makes the node register and quit), * and then re-starts the node with the given parameters. + * @param rootCertificate if not null every time a node is started for registration that certificate is written on disk * @param dsl The dsl itself. * @return The value returned in the [dsl] closure. */ +// TODO: make the registration testing parameters internal. fun driver( defaultParameters: DriverParameters = DriverParameters(), isDebug: Boolean = defaultParameters.isDebug, @@ -360,6 +364,7 @@ fun driver( notarySpecs: List = defaultParameters.notarySpecs, extraCordappPackagesToScan: List = defaultParameters.extraCordappPackagesToScan, compatibilityZoneURL: URL? = defaultParameters.compatibilityZoneURL, + rootCertificate: X509CertificateHolder? = defaultParameters.rootCertificate, dsl: DriverDSLExposedInterface.() -> A ): A { return genericDriver( @@ -374,7 +379,8 @@ fun driver( waitForNodesToFinish = waitForAllNodesToFinish, notarySpecs = notarySpecs, extraCordappPackagesToScan = extraCordappPackagesToScan, - compatibilityZoneURL = compatibilityZoneURL + compatibilityZoneURL = compatibilityZoneURL, + rootCertificate = rootCertificate ), coerce = { it }, dsl = dsl, @@ -410,7 +416,8 @@ data class DriverParameters( val waitForNodesToFinish: Boolean = false, val notarySpecs: List = listOf(NotarySpec(DUMMY_NOTARY.name)), val extraCordappPackagesToScan: List = emptyList(), - val compatibilityZoneURL: URL? = null + val compatibilityZoneURL: URL? = null, + val rootCertificate: X509CertificateHolder? = null ) { fun setIsDebug(isDebug: Boolean) = copy(isDebug = isDebug) fun setDriverDirectory(driverDirectory: Path) = copy(driverDirectory = driverDirectory) @@ -421,8 +428,10 @@ data class DriverParameters( fun setInitialiseSerialization(initialiseSerialization: Boolean) = copy(initialiseSerialization = initialiseSerialization) fun setStartNodesInProcess(startNodesInProcess: Boolean) = copy(startNodesInProcess = startNodesInProcess) fun setTerminateNodesOnShutdown(terminateNodesOnShutdown: Boolean) = copy(waitForNodesToFinish = terminateNodesOnShutdown) - fun setExtraCordappPackagesToScan(extraCordappPackagesToScan: List) = copy(extraCordappPackagesToScan = extraCordappPackagesToScan) fun setNotarySpecs(notarySpecs: List) = copy(notarySpecs = notarySpecs) + fun setExtraCordappPackagesToScan(extraCordappPackagesToScan: List) = copy(extraCordappPackagesToScan = extraCordappPackagesToScan) + fun setCompatibilityZoneURL(compatibilityZoneURL: URL?) = copy(compatibilityZoneURL = compatibilityZoneURL) + fun setRootCertificate(rootCertificate: X509CertificateHolder?) = copy(rootCertificate = rootCertificate) } /** @@ -476,6 +485,7 @@ fun genericD notarySpecs: List, extraCordappPackagesToScan: List = defaultParameters.extraCordappPackagesToScan, compatibilityZoneURL: URL? = defaultParameters.compatibilityZoneURL, + rootCertificate: X509CertificateHolder? = defaultParameters.rootCertificate, driverDslWrapper: (DriverDSL) -> D, coerce: (D) -> DI, dsl: DI.() -> A ): A { @@ -492,7 +502,8 @@ fun genericD waitForNodesToFinish = waitForNodesToFinish, extraCordappPackagesToScan = extraCordappPackagesToScan, notarySpecs = notarySpecs, - compatibilityZoneURL = compatibilityZoneURL + compatibilityZoneURL = compatibilityZoneURL, + rootCertificate = rootCertificate ) ) val shutdownHook = addShutdownHook(driverDsl::shutdown) @@ -599,7 +610,8 @@ class DriverDSL( val waitForNodesToFinish: Boolean, extraCordappPackagesToScan: List, val notarySpecs: List, - val compatibilityZoneURL: URL? + val compatibilityZoneURL: URL?, + val rootCertificate: X509CertificateHolder? ) : DriverDSLInternalInterface { private var _executorService: ScheduledExecutorService? = null val executorService get() = _executorService!! @@ -711,6 +723,11 @@ class DriverDSL( } } + private fun writeRootCaCertificateForNode(path: Path, caRootCertificate: X509CertificateHolder) { + path.parent.createDirectories() + X509Utilities.saveCertificateAsPEMFile(caRootCertificate, path) + } + private fun registerNode(providedName: CordaX500Name, compatibilityZoneURL: URL): CordaFuture { val config = ConfigHelper.loadConfig( baseDirectory = baseDirectory(providedName), @@ -720,10 +737,12 @@ class DriverDSL( "compatibilityZoneURL" to compatibilityZoneURL.toString(), "myLegalName" to providedName.toString()) ) + val configuration = config.parseAsNodeConfiguration() + // If a rootCertificate is specified, put that in the node expected path. + rootCertificate?.let { writeRootCaCertificateForNode(configuration.rootCaCertFile, it) } if (startNodesInProcess) { // This is a bit cheating, we're not starting a full node, we're just calling the code nodes call // when registering. - val configuration = config.parseAsNodeConfiguration() NetworkRegistrationHelper(configuration, HTTPNetworkRegistrationService(compatibilityZoneURL)) .buildKeystore() return doneFuture(Unit) @@ -857,12 +876,14 @@ class DriverDSL( ServiceIdentityGenerator.generateToDisk( dirs = listOf(baseDirectory(spec.name)), serviceName = spec.name, + rootCertertificate = rootCertificate, serviceId = "identity" ) } else { ServiceIdentityGenerator.generateToDisk( dirs = generateNodeNames(spec).map { baseDirectory(it) }, serviceName = spec.name, + rootCertertificate = rootCertificate, serviceId = NotaryService.constructId( validating = spec.validating, raft = spec.cluster is ClusterSpec.Raft diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/internal/RPCDriver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/internal/RPCDriver.kt index 0646c8423b..1dec5f7076 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/internal/RPCDriver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/internal/RPCDriver.kt @@ -47,6 +47,7 @@ import org.apache.activemq.artemis.core.settings.impl.AddressFullMessagePolicy import org.apache.activemq.artemis.core.settings.impl.AddressSettings import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection import org.apache.activemq.artemis.spi.core.security.ActiveMQSecurityManager3 +import org.bouncycastle.cert.X509CertificateHolder import java.lang.reflect.Method import java.net.URL import java.nio.file.Path @@ -237,6 +238,7 @@ fun rpcDriver( notarySpecs: List = emptyList(), externalTrace: Trace? = null, compatibilityZoneURL: URL? = null, + rootCertificate: X509CertificateHolder? = null, dsl: RPCDriverExposedDSLInterface.() -> A ) = genericDriver( driverDsl = RPCDriverDSL( @@ -251,7 +253,8 @@ fun rpcDriver( waitForNodesToFinish = waitForNodesToFinish, extraCordappPackagesToScan = extraCordappPackagesToScan, notarySpecs = notarySpecs, - compatibilityZoneURL = compatibilityZoneURL + compatibilityZoneURL = compatibilityZoneURL, + rootCertificate = rootCertificate ), externalTrace ), coerce = { it }, diff --git a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt index 3e88638771..61b80faa6b 100644 --- a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt +++ b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt @@ -37,6 +37,7 @@ import org.apache.activemq.artemis.core.security.CheckType import org.apache.activemq.artemis.core.security.Role import org.apache.activemq.artemis.core.server.impl.ActiveMQServerImpl import org.apache.activemq.artemis.spi.core.security.ActiveMQSecurityManager +import org.bouncycastle.cert.X509CertificateHolder import java.net.URL import java.nio.file.Path import java.nio.file.Paths @@ -87,6 +88,7 @@ fun verifierDriver( extraCordappPackagesToScan: List = emptyList(), notarySpecs: List = emptyList(), compatibilityZoneURL: URL? = null, + rootCertificate: X509CertificateHolder? = null, dsl: VerifierExposedDSLInterface.() -> A ) = genericDriver( driverDsl = VerifierDriverDSL( @@ -101,7 +103,8 @@ fun verifierDriver( waitForNodesToFinish = waitForNodesToFinish, extraCordappPackagesToScan = extraCordappPackagesToScan, notarySpecs = notarySpecs, - compatibilityZoneURL = compatibilityZoneURL + compatibilityZoneURL = compatibilityZoneURL, + rootCertificate = rootCertificate ) ), coerce = { it },