From ab80df342ab8a7ede0539daaee9fcecd02f6aeeb Mon Sep 17 00:00:00 2001 From: Michal Kit Date: Mon, 30 Apr 2018 09:26:26 +0100 Subject: [PATCH] CORDA-1319 Adding CRL checking for nodes (#2987) * Adding CRL support for nodes * Addressing review comments --- .../net/corda/core/internal/InternalUtils.kt | 7 +- docs/source/corda-configuration-file.rst | 5 + .../src/main/resources/example-node.conf | 1 + .../nodeapi/internal/DevIdentityGenerator.kt | 1 + .../internal/bridging/AMQPBridgeManager.kt | 6 +- .../internal/config/SSLConfiguration.kt | 1 + .../nodeapi/internal/crypto/X509Utilities.kt | 2 +- .../protonwrapper/netty/AMQPClient.kt | 7 +- .../protonwrapper/netty/AMQPServer.kt | 12 +- .../internal/protonwrapper/netty/SSLHelper.kt | 30 +- .../internal/crypto/X509UtilitiesTest.kt | 2 + .../net/corda/node/NodeKeystoreCheckTest.kt | 1 + .../net/corda/node/amqp/AMQPBridgeTest.kt | 4 +- .../CertificateRevocationListNodeTests.kt | 473 ++++++++++++++++++ .../net/corda/node/amqp/ProtonWrapperTests.kt | 18 +- .../messaging/MQSecurityAsNodeTest.kt | 1 + .../node/services/config/NodeConfiguration.kt | 14 +- .../corda/node/services/config/SslOptions.kt | 15 +- .../node/services/config/shell/ShellConfig.kt | 4 +- node/src/main/resources/reference.conf | 1 + .../config/NodeConfigurationImplTest.kt | 5 +- .../internal/UnsafeCertificatesFactory.kt | 9 +- .../testing/internal/InternalTestUtils.kt | 11 +- .../shell/InteractiveShellIntegrationTest.kt | 6 +- .../corda/tools/shell/ShellConfiguration.kt | 8 +- .../tools/shell/StandaloneShellArgsParser.kt | 10 +- .../shell/StandaloneShellArgsParserTest.kt | 11 +- .../net/corda/webserver/WebServerConfig.kt | 1 + 28 files changed, 609 insertions(+), 57 deletions(-) create mode 100644 node/src/integration-test/kotlin/net/corda/node/amqp/CertificateRevocationListNodeTests.kt diff --git a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt index c912680484..eafbaea2ac 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -350,13 +350,14 @@ fun ExecutorService.join() { } } -fun CertPath.validate(trustAnchor: TrustAnchor): PKIXCertPathValidatorResult { - val parameters = PKIXParameters(setOf(trustAnchor)).apply { isRevocationEnabled = false } +// TODO: Currently the certificate revocation status is not handled here. Nowhere in the code the second parameter is used. Consider adding the support in the future. +fun CertPath.validate(trustAnchor: TrustAnchor, checkRevocation: Boolean = false): PKIXCertPathValidatorResult { + val parameters = PKIXParameters(setOf(trustAnchor)).apply { isRevocationEnabled = checkRevocation } try { return CertPathValidator.getInstance("PKIX").validate(this, parameters) as PKIXCertPathValidatorResult } catch (e: CertPathValidatorException) { throw CertPathValidatorException( - """Cert path failed to validate against trust anchor. + """Cert path failed to validate. Reason: ${e.reason} Offending cert index: ${e.index} Cert path: $this diff --git a/docs/source/corda-configuration-file.rst b/docs/source/corda-configuration-file.rst index cf9942bcb9..c28c30a392 100644 --- a/docs/source/corda-configuration-file.rst +++ b/docs/source/corda-configuration-file.rst @@ -67,6 +67,11 @@ absolute path to the node's base directory. .. note:: Longer term these keys will be managed in secure hardware devices. +:crlCheckSoftFail: This is a boolean flag that when enabled (i.e. `true` value is set) the certificate revocation list (CRL) checking will use the soft fail mode. + The soft fail mode allows the revocation check to succeed if the revocation status cannot be determined because of a network error. + If this parameter is set to `false` the rigorous CRL checking takes place, meaning that each certificate in the + certificate path being checked needs to have the CRL distribution point extension set and pointing to a URL serving a valid CRL. + :database: Database configuration: :serverNameTablePrefix: Prefix string to apply to all the database tables. The default is no prefix. diff --git a/docs/source/example-code/src/main/resources/example-node.conf b/docs/source/example-code/src/main/resources/example-node.conf index 1d3075391b..918b574d84 100644 --- a/docs/source/example-code/src/main/resources/example-node.conf +++ b/docs/source/example-code/src/main/resources/example-node.conf @@ -1,6 +1,7 @@ myLegalName : "O=Bank A,L=London,C=GB" keyStorePassword : "cordacadevpass" trustStorePassword : "trustpass" +crlCheckSoftFail: true dataSourceProperties : { dataSourceClassName : org.h2.jdbcx.JdbcDataSource dataSource.url : "jdbc:h2:file:"${baseDirectory}"/persistence" diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/DevIdentityGenerator.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/DevIdentityGenerator.kt index cc0563bef3..080bf680c4 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/DevIdentityGenerator.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/DevIdentityGenerator.kt @@ -35,6 +35,7 @@ object DevIdentityGenerator { override val baseDirectory = nodeDir override val keyStorePassword: String = "cordacadevpass" override val trustStorePassword get() = throw NotImplementedError("Not expected to be called") + override val crlCheckSoftFail: Boolean = true } nodeSslConfig.certificatesDirectory.createDirectories() diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/AMQPBridgeManager.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/AMQPBridgeManager.kt index 2c771cfd13..a5a2b94675 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/AMQPBridgeManager.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/AMQPBridgeManager.kt @@ -46,6 +46,7 @@ class AMQPBridgeManager(config: NodeSSLConfiguration, val artemisMessageClientFa private val keyStorePrivateKeyPassword: String = config.keyStorePassword private val trustStore = config.loadTrustStore().internal private var artemis: ArtemisSessionProvider? = null + private val crlCheckSoftFail: Boolean = config.crlCheckSoftFail constructor(config: NodeSSLConfiguration, p2pAddress: NetworkHostAndPort, maxMessageSize: Int) : this(config, { ArtemisMessagingClient(config, p2pAddress, maxMessageSize) }) @@ -67,6 +68,7 @@ class AMQPBridgeManager(config: NodeSSLConfiguration, val artemisMessageClientFa keyStore: KeyStore, keyStorePrivateKeyPassword: String, trustStore: KeyStore, + crlCheckSoftFail: Boolean, sharedEventGroup: EventLoopGroup, private val artemis: ArtemisSessionProvider) { companion object { @@ -75,7 +77,7 @@ class AMQPBridgeManager(config: NodeSSLConfiguration, val artemisMessageClientFa private val log = LoggerFactory.getLogger("$bridgeName:${legalNames.first()}") - val amqpClient = AMQPClient(listOf(target), legalNames, PEER_USER, PEER_USER, keyStore, keyStorePrivateKeyPassword, trustStore, sharedThreadPool = sharedEventGroup) + val amqpClient = AMQPClient(listOf(target), legalNames, PEER_USER, PEER_USER, keyStore, keyStorePrivateKeyPassword, trustStore, crlCheckSoftFail, sharedThreadPool = sharedEventGroup) val bridgeName: String get() = getBridgeName(queueName, target) private val lock = ReentrantLock() // lock to serialise session level access private var session: ClientSession? = null @@ -169,7 +171,7 @@ class AMQPBridgeManager(config: NodeSSLConfiguration, val artemisMessageClientFa if (bridgeExists(getBridgeName(queueName, target))) { return } - val newBridge = AMQPBridge(queueName, target, legalNames, keyStore, keyStorePrivateKeyPassword, trustStore, sharedEventLoopGroup!!, artemis!!) + val newBridge = AMQPBridge(queueName, target, legalNames, keyStore, keyStorePrivateKeyPassword, trustStore, crlCheckSoftFail, sharedEventLoopGroup!!, artemis!!) lock.withLock { bridgeNameToBridgeMap[newBridge.bridgeName] = newBridge } 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 f44b190d5a..e8c63fc1f3 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 @@ -12,6 +12,7 @@ interface SSLConfiguration { // TODO This looks like it should be in NodeSSLConfiguration val nodeKeystore: Path get() = certificatesDirectory / "nodekeystore.jks" val trustStoreFile: Path get() = certificatesDirectory / "truststore.jks" + val crlCheckSoftFail: Boolean fun loadTrustStore(createNew: Boolean = false): X509KeyStore { return X509KeyStore.fromFile(trustStoreFile, trustStorePassword, createNew) 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 85e00d7807..5d881aa9dc 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 @@ -148,7 +148,7 @@ object X509Utilities { * @param crlDistPoint CRL distribution point. * @param crlIssuer X500Name of the CRL issuer. */ - private fun createPartialCertificate(certificateType: CertificateType, + fun createPartialCertificate(certificateType: CertificateType, issuer: X500Principal, issuerPublicKey: PublicKey, subject: X500Principal, diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPClient.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPClient.kt index 7e8a8853f7..62838ba626 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPClient.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPClient.kt @@ -38,6 +38,7 @@ class AMQPClient(val targets: List, private val keyStore: KeyStore, private val keyStorePrivateKeyPassword: String, private val trustStore: KeyStore, + private val crlCheckSoftFail: Boolean, private val trace: Boolean = false, private val sharedThreadPool: EventLoopGroup? = null) : AutoCloseable { companion object { @@ -102,7 +103,7 @@ class AMQPClient(val targets: List, init { keyManagerFactory.init(parent.keyStore, parent.keyStorePrivateKeyPassword.toCharArray()) - trustManagerFactory.init(parent.trustStore) + trustManagerFactory.init(initialiseTrustStoreAndEnableCrlChecking(parent.trustStore, parent.crlCheckSoftFail)) } override fun initChannel(ch: SocketChannel) { @@ -132,9 +133,7 @@ class AMQPClient(val targets: List, private fun restart() { val bootstrap = Bootstrap() // TODO Needs more configuration control when we profile. e.g. to use EPOLL on Linux - bootstrap.group(workerGroup). - channel(NioSocketChannel::class.java). - handler(ClientChannelInitializer(this)) + bootstrap.group(workerGroup).channel(NioSocketChannel::class.java).handler(ClientChannelInitializer(this)) currentTarget = targets[targetIndex] val clientFuture = bootstrap.connect(currentTarget.host, currentTarget.port) clientFuture.addListener(connectListener) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPServer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPServer.kt index 9c98767d0e..007729f512 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPServer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPServer.kt @@ -40,6 +40,7 @@ class AMQPServer(val hostName: String, private val keyStore: KeyStore, private val keyStorePrivateKeyPassword: CharArray, private val trustStore: KeyStore, + private val crlCheckSoftFail: Boolean, private val trace: Boolean = false) : AutoCloseable { companion object { @@ -66,7 +67,8 @@ class AMQPServer(val hostName: String, keyStore: KeyStore, keyStorePrivateKeyPassword: String, trustStore: KeyStore, - trace: Boolean = false) : this(hostName, port, userName, password, keyStore, keyStorePrivateKeyPassword.toCharArray(), trustStore, trace) + crlCheckSoftFail: Boolean, + trace: Boolean = false) : this(hostName, port, userName, password, keyStore, keyStorePrivateKeyPassword.toCharArray(), trustStore, crlCheckSoftFail, trace) private class ServerChannelInitializer(val parent: AMQPServer) : ChannelInitializer() { private val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()) @@ -74,7 +76,7 @@ class AMQPServer(val hostName: String, init { keyManagerFactory.init(parent.keyStore, parent.keyStorePrivateKeyPassword) - trustManagerFactory.init(parent.trustStore) + trustManagerFactory.init(initialiseTrustStoreAndEnableCrlChecking(parent.trustStore, parent.crlCheckSoftFail)) } override fun initChannel(ch: SocketChannel) { @@ -108,11 +110,7 @@ class AMQPServer(val hostName: String, val server = ServerBootstrap() // TODO Needs more configuration control when we profile. e.g. to use EPOLL on Linux - server.group(bossGroup, workerGroup). - channel(NioServerSocketChannel::class.java). - option(ChannelOption.SO_BACKLOG, 100). - handler(LoggingHandler(LogLevel.INFO)). - childHandler(ServerChannelInitializer(this)) + server.group(bossGroup, workerGroup).channel(NioServerSocketChannel::class.java).option(ChannelOption.SO_BACKLOG, 100).handler(LoggingHandler(LogLevel.INFO)).childHandler(ServerChannelInitializer(this)) log.info("Try to bind $port") val channelFuture = server.bind(hostName, port).sync() // block/throw here as better to know we failed to claim port than carry on diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SSLHelper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SSLHelper.kt index bf1be88377..689211fc30 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SSLHelper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SSLHelper.kt @@ -3,10 +3,14 @@ package net.corda.nodeapi.internal.protonwrapper.netty import io.netty.handler.ssl.SslHandler import net.corda.core.utilities.NetworkHostAndPort import net.corda.nodeapi.ArtemisTcpTransport +import java.security.KeyStore import java.security.SecureRandom -import javax.net.ssl.KeyManagerFactory -import javax.net.ssl.SSLContext -import javax.net.ssl.TrustManagerFactory +import java.security.cert.CertPathBuilder +import java.security.cert.PKIXBuilderParameters +import java.security.cert.PKIXRevocationChecker +import java.security.cert.X509CertSelector +import java.util.* +import javax.net.ssl.* internal fun createClientSslHelper(target: NetworkHostAndPort, keyManagerFactory: KeyManagerFactory, @@ -36,4 +40,22 @@ internal fun createServerSslHelper(keyManagerFactory: KeyManagerFactory, sslEngine.enabledCipherSuites = ArtemisTcpTransport.CIPHER_SUITES.toTypedArray() sslEngine.enableSessionCreation = true return SslHandler(sslEngine) -} \ No newline at end of file +} + +internal fun initialiseTrustStoreAndEnableCrlChecking(trustStore: KeyStore, crlCheckSoftFail: Boolean): ManagerFactoryParameters { + val certPathBuilder = CertPathBuilder.getInstance("PKIX") + val revocationChecker = certPathBuilder.revocationChecker as PKIXRevocationChecker + revocationChecker.options = EnumSet.of( + // Prefer CRL over OCSP + PKIXRevocationChecker.Option.PREFER_CRLS, + // Don't fall back to OCSP checking + PKIXRevocationChecker.Option.NO_FALLBACK) + if (crlCheckSoftFail) { + // Allow revocation check to succeed if the revocation status cannot be determined for one of + // the following reasons: The CRL or OCSP response cannot be obtained because of a network error. + revocationChecker.options = revocationChecker.options + PKIXRevocationChecker.Option.SOFT_FAIL + } + val pkixParams = PKIXBuilderParameters(trustStore, X509CertSelector()) + pkixParams.addCertPathChecker(revocationChecker) + return CertPathTrustManagerParameters(pkixParams) +} diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt index 1fbd9e1dad..5a76745aee 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt @@ -183,6 +183,7 @@ class X509UtilitiesTest { override val certificatesDirectory = tempFolder.root.toPath() override val keyStorePassword = "serverstorepass" override val trustStorePassword = "trustpass" + override val crlCheckSoftFail: Boolean = true } val (rootCa, intermediateCa) = createDevIntermediateCaCertPath() @@ -218,6 +219,7 @@ class X509UtilitiesTest { override val certificatesDirectory = tempFolder.root.toPath() override val keyStorePassword = "serverstorepass" override val trustStorePassword = "trustpass" + override val crlCheckSoftFail: Boolean = true } val (rootCa, intermediateCa) = createDevIntermediateCaCertPath() diff --git a/node/src/integration-test/kotlin/net/corda/node/NodeKeystoreCheckTest.kt b/node/src/integration-test/kotlin/net/corda/node/NodeKeystoreCheckTest.kt index 392f835a04..7800bf67b2 100644 --- a/node/src/integration-test/kotlin/net/corda/node/NodeKeystoreCheckTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/NodeKeystoreCheckTest.kt @@ -34,6 +34,7 @@ class NodeKeystoreCheckTest { override val keyStorePassword: String = keystorePassword override val trustStorePassword: String = keystorePassword override val certificatesDirectory: Path = baseDirectory(ALICE_NAME) / "certificates" + override val crlCheckSoftFail: Boolean = true } config.configureDevKeyAndTrustStores(ALICE_NAME) diff --git a/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt b/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt index 93b18d7983..95075e6bdb 100644 --- a/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt @@ -77,7 +77,7 @@ class AMQPBridgeTest { fun formatMessage(expected: String, actual: Int, received: List): String { return "Expected message with id $expected, got $actual, previous message receive sequence: " + - "${received.joinToString(", ", "[", "]")}." + "${received.joinToString(", ", "[", "]")}." } val received1 = receive.next() @@ -173,6 +173,7 @@ class AMQPBridgeTest { doReturn(temporaryFolder.root.toPath() / "artemis").whenever(it).baseDirectory doReturn(ALICE_NAME).whenever(it).myLegalName doReturn("trustpass").whenever(it).trustStorePassword + doReturn(true).whenever(it).crlCheckSoftFail doReturn("cordacadevpass").whenever(it).keyStorePassword doReturn(artemisAddress).whenever(it).p2pAddress doReturn(null).whenever(it).jmxMonitoringHttpPort @@ -210,6 +211,7 @@ class AMQPBridgeTest { serverConfig.loadSslKeyStore().internal, serverConfig.keyStorePassword, serverConfig.loadTrustStore().internal, + crlCheckSoftFail = true, trace = true ) } diff --git a/node/src/integration-test/kotlin/net/corda/node/amqp/CertificateRevocationListNodeTests.kt b/node/src/integration-test/kotlin/net/corda/node/amqp/CertificateRevocationListNodeTests.kt new file mode 100644 index 0000000000..dd8240c822 --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/amqp/CertificateRevocationListNodeTests.kt @@ -0,0 +1,473 @@ +package net.corda.node.amqp + +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.whenever +import net.corda.core.crypto.Crypto +import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.div +import net.corda.core.toFuture +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.days +import net.corda.core.utilities.minutes +import net.corda.core.utilities.seconds +import net.corda.node.services.config.NodeConfiguration +import net.corda.node.services.config.configureWithDevSSLCertificate +import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX +import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER +import net.corda.nodeapi.internal.config.SSLConfiguration +import net.corda.nodeapi.internal.crypto.* +import net.corda.nodeapi.internal.protonwrapper.messages.MessageStatus +import net.corda.nodeapi.internal.protonwrapper.netty.AMQPClient +import net.corda.nodeapi.internal.protonwrapper.netty.AMQPServer +import net.corda.testing.core.ALICE_NAME +import net.corda.testing.core.BOB_NAME +import net.corda.testing.core.CHARLIE_NAME +import net.corda.testing.core.freePort +import net.corda.testing.internal.DEV_INTERMEDIATE_CA +import net.corda.testing.internal.DEV_ROOT_CA +import net.corda.testing.internal.rigorousMock +import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.asn1.x509.* +import org.bouncycastle.cert.jcajce.JcaX509CRLConverter +import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils +import org.bouncycastle.cert.jcajce.JcaX509v2CRLBuilder +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder +import org.eclipse.jetty.server.Server +import org.eclipse.jetty.server.ServerConnector +import org.eclipse.jetty.server.handler.HandlerCollection +import org.eclipse.jetty.servlet.ServletContextHandler +import org.eclipse.jetty.servlet.ServletHolder +import org.glassfish.jersey.server.ResourceConfig +import org.glassfish.jersey.servlet.ServletContainer +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.Closeable +import java.math.BigInteger +import java.net.InetSocketAddress +import java.security.KeyPair +import java.security.PrivateKey +import java.security.Security +import java.security.cert.X509CRL +import java.security.cert.X509Certificate +import java.util.* +import javax.ws.rs.GET +import javax.ws.rs.Path +import javax.ws.rs.Produces +import javax.ws.rs.core.Response +import kotlin.test.assertEquals + +class CertificateRevocationListNodeTests { + @Rule + @JvmField + val temporaryFolder = TemporaryFolder() + + private val ROOT_CA = DEV_ROOT_CA + private lateinit var INTERMEDIATE_CA: CertificateAndKeyPair + + private val serverPort = freePort() + + private lateinit var server: CrlServer + + private val revokedNodeCerts: MutableList = mutableListOf() + private val revokedIntermediateCerts: MutableList = mutableListOf() + + private abstract class AbstractNodeConfiguration : NodeConfiguration + + @Before + fun setUp() { + Security.addProvider(BouncyCastleProvider()) + revokedNodeCerts.clear() + server = CrlServer(NetworkHostAndPort("localhost", 0)) + server.start() + INTERMEDIATE_CA = CertificateAndKeyPair(replaceCrlDistPointCaCertificate( + DEV_INTERMEDIATE_CA.certificate, + CertificateType.INTERMEDIATE_CA, + ROOT_CA.keyPair, + "http://${server.hostAndPort}/crl/intermediate.crl"), DEV_INTERMEDIATE_CA.keyPair) + } + + @After + fun tearDown() { + server.close() + revokedNodeCerts.clear() + } + + @Test + fun `Simple AMPQ Client to Server connection works`() { + val crlCheckSoftFail = true + val (amqpServer, _) = createServer(serverPort, crlCheckSoftFail = crlCheckSoftFail) + amqpServer.use { + amqpServer.start() + val receiveSubs = amqpServer.onReceive.subscribe { + assertEquals(BOB_NAME.toString(), it.sourceLegalName) + assertEquals(P2P_PREFIX + "Test", it.topic) + assertEquals("Test", String(it.payload)) + it.complete(true) + } + val (amqpClient, _) = createClient(serverPort, crlCheckSoftFail) + amqpClient.use { + val serverConnected = amqpServer.onConnection.toFuture() + val clientConnected = amqpClient.onConnection.toFuture() + amqpClient.start() + val serverConnect = serverConnected.get() + assertEquals(true, serverConnect.connected) + val clientConnect = clientConnected.get() + assertEquals(true, clientConnect.connected) + val msg = amqpClient.createMessage("Test".toByteArray(), + P2P_PREFIX + "Test", + ALICE_NAME.toString(), + emptyMap()) + amqpClient.write(msg) + assertEquals(MessageStatus.Acknowledged, msg.onComplete.get()) + receiveSubs.unsubscribe() + } + } + } + + @Test + fun `AMPQ Client to Server connection fails when client's certificate is revoked`() { + val crlCheckSoftFail = true + val (amqpServer, _) = createServer(serverPort, crlCheckSoftFail = crlCheckSoftFail) + amqpServer.use { + amqpServer.start() + amqpServer.onReceive.subscribe { + it.complete(true) + } + val (amqpClient, clientCert) = createClient(serverPort, crlCheckSoftFail) + revokedNodeCerts.add(clientCert.serialNumber) + amqpClient.use { + val serverConnected = amqpServer.onConnection.toFuture() + amqpClient.onConnection.toFuture() + amqpClient.start() + val serverConnect = serverConnected.get() + assertEquals(false, serverConnect.connected) + } + } + } + + @Test + fun `AMPQ Client to Server connection fails when servers's certificate is revoked`() { + val crlCheckSoftFail = true + val (amqpServer, serverCert) = createServer(serverPort, crlCheckSoftFail = crlCheckSoftFail) + revokedNodeCerts.add(serverCert.serialNumber) + amqpServer.use { + amqpServer.start() + amqpServer.onReceive.subscribe { + it.complete(true) + } + val (amqpClient, _) = createClient(serverPort, crlCheckSoftFail) + amqpClient.use { + val serverConnected = amqpServer.onConnection.toFuture() + amqpClient.onConnection.toFuture() + amqpClient.start() + val serverConnect = serverConnected.get() + assertEquals(false, serverConnect.connected) + } + } + } + + @Test + fun `AMPQ Client to Server connection fails when servers's certificate is revoked and soft fail is enabled`() { + val crlCheckSoftFail = true + val (amqpServer, serverCert) = createServer(serverPort, crlCheckSoftFail = crlCheckSoftFail) + revokedNodeCerts.add(serverCert.serialNumber) + amqpServer.use { + amqpServer.start() + amqpServer.onReceive.subscribe { + it.complete(true) + } + val (amqpClient, _) = createClient(serverPort, crlCheckSoftFail) + amqpClient.use { + val serverConnected = amqpServer.onConnection.toFuture() + amqpClient.onConnection.toFuture() + amqpClient.start() + val serverConnect = serverConnected.get() + assertEquals(false, serverConnect.connected) + } + } + } + + @Test + fun `AMPQ Client to Server connection succeeds when CRL cannot be obtained and soft fail is enabled`() { + val crlCheckSoftFail = true + val (amqpServer, serverCert) = createServer( + serverPort, + crlCheckSoftFail = crlCheckSoftFail, + nodeCrlDistPoint = "http://${server.hostAndPort}/crl/invalid.crl") + amqpServer.use { + amqpServer.start() + amqpServer.onReceive.subscribe { + it.complete(true) + } + val (amqpClient, _) = createClient( + serverPort, + crlCheckSoftFail, + nodeCrlDistPoint = "http://${server.hostAndPort}/crl/invalid.crl") + amqpClient.use { + val serverConnected = amqpServer.onConnection.toFuture() + amqpClient.onConnection.toFuture() + amqpClient.start() + val serverConnect = serverConnected.get() + assertEquals(true, serverConnect.connected) + } + } + } + + @Test + fun `Revocation status chceck fails when the CRL distribution point is not set and soft fail is disabled`() { + val crlCheckSoftFail = false + val (amqpServer, _) = createServer( + serverPort, + crlCheckSoftFail = crlCheckSoftFail, + tlsCrlDistPoint = null) + amqpServer.use { + amqpServer.start() + amqpServer.onReceive.subscribe { + it.complete(true) + } + val (amqpClient, _) = createClient( + serverPort, + crlCheckSoftFail, + tlsCrlDistPoint = null) + amqpClient.use { + val serverConnected = amqpServer.onConnection.toFuture() + amqpClient.onConnection.toFuture() + amqpClient.start() + val serverConnect = serverConnected.get() + assertEquals(false, serverConnect.connected) + } + } + } + + @Test + fun `Revocation status chceck succeds when the CRL distribution point is not set and soft fail is enabled`() { + val crlCheckSoftFail = true + val (amqpServer, _) = createServer( + serverPort, + crlCheckSoftFail = crlCheckSoftFail, + tlsCrlDistPoint = null) + amqpServer.use { + amqpServer.start() + amqpServer.onReceive.subscribe { + it.complete(true) + } + val (amqpClient, _) = createClient( + serverPort, + crlCheckSoftFail, + tlsCrlDistPoint = null) + amqpClient.use { + val serverConnected = amqpServer.onConnection.toFuture() + amqpClient.onConnection.toFuture() + amqpClient.start() + val serverConnect = serverConnected.get() + assertEquals(true, serverConnect.connected) + } + } + } + + private fun createClient(targetPort: Int, + crlCheckSoftFail: Boolean, + nodeCrlDistPoint: String = "http://${server.hostAndPort}/crl/node.crl", + tlsCrlDistPoint: String? = "http://${server.hostAndPort}/crl/empty.crl"): Pair { + val clientConfig = rigorousMock().also { + doReturn(temporaryFolder.root.toPath() / "client").whenever(it).baseDirectory + doReturn(BOB_NAME).whenever(it).myLegalName + doReturn("trustpass").whenever(it).trustStorePassword + doReturn("cordacadevpass").whenever(it).keyStorePassword + doReturn(crlCheckSoftFail).whenever(it).crlCheckSoftFail + } + clientConfig.configureWithDevSSLCertificate() + val nodeCert = clientConfig.recreateNodeCaAndTlsCertificates(nodeCrlDistPoint, tlsCrlDistPoint) + val clientTruststore = clientConfig.loadTrustStore().internal + val clientKeystore = clientConfig.loadSslKeyStore().internal + return Pair(AMQPClient( + listOf(NetworkHostAndPort("localhost", targetPort)), + setOf(ALICE_NAME, CHARLIE_NAME), + PEER_USER, + PEER_USER, + clientKeystore, + clientConfig.keyStorePassword, + clientTruststore, + crlCheckSoftFail), nodeCert) + } + + private fun createServer(port: Int, name: CordaX500Name = ALICE_NAME, + crlCheckSoftFail: Boolean, + nodeCrlDistPoint: String = "http://${server.hostAndPort}/crl/node.crl", + tlsCrlDistPoint: String? = "http://${server.hostAndPort}/crl/empty.crl"): Pair { + val serverConfig = rigorousMock().also { + doReturn(temporaryFolder.root.toPath() / "server").whenever(it).baseDirectory + doReturn(name).whenever(it).myLegalName + doReturn("trustpass").whenever(it).trustStorePassword + doReturn("cordacadevpass").whenever(it).keyStorePassword + doReturn(crlCheckSoftFail).whenever(it).crlCheckSoftFail + } + serverConfig.configureWithDevSSLCertificate() + val nodeCert = serverConfig.recreateNodeCaAndTlsCertificates(nodeCrlDistPoint, tlsCrlDistPoint) + val serverTruststore = serverConfig.loadTrustStore().internal + val serverKeystore = serverConfig.loadSslKeyStore().internal + return Pair(AMQPServer( + "0.0.0.0", + port, + PEER_USER, + PEER_USER, + serverKeystore, + serverConfig.keyStorePassword, + serverTruststore, + crlCheckSoftFail), nodeCert) + } + + private fun SSLConfiguration.recreateNodeCaAndTlsCertificates(nodeCaCrlDistPoint: String, tlsCrlDistPoint: String?): X509Certificate { + val nodeKeyStore = loadNodeKeyStore() + val (nodeCert, nodeKeys) = nodeKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA) + val newNodeCert = replaceCrlDistPointCaCertificate(nodeCert, CertificateType.NODE_CA, INTERMEDIATE_CA.keyPair, nodeCaCrlDistPoint) + val nodeCertChain = listOf(newNodeCert, INTERMEDIATE_CA.certificate, *nodeKeyStore.getCertificateChain(X509Utilities.CORDA_CLIENT_CA).drop(2).toTypedArray()) + nodeKeyStore.internal.deleteEntry(X509Utilities.CORDA_CLIENT_CA) + nodeKeyStore.save() + nodeKeyStore.update { + setPrivateKey(X509Utilities.CORDA_CLIENT_CA, nodeKeys.private, nodeCertChain) + } + val sslKeyStore = loadSslKeyStore() + val (tlsCert, tlsKeys) = sslKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_TLS) + val newTlsCert = replaceCrlDistPointCaCertificate(tlsCert, CertificateType.TLS, nodeKeys, tlsCrlDistPoint, X500Name.getInstance(ROOT_CA.certificate.subjectX500Principal.encoded)) + val sslCertChain = listOf(newTlsCert, newNodeCert, INTERMEDIATE_CA.certificate, *sslKeyStore.getCertificateChain(X509Utilities.CORDA_CLIENT_TLS).drop(3).toTypedArray()) + sslKeyStore.internal.deleteEntry(X509Utilities.CORDA_CLIENT_TLS) + sslKeyStore.save() + sslKeyStore.update { + setPrivateKey(X509Utilities.CORDA_CLIENT_TLS, tlsKeys.private, sslCertChain) + } + return newNodeCert + } + + private fun replaceCrlDistPointCaCertificate(currentCaCert: X509Certificate, certType: CertificateType, issuerKeyPair: KeyPair, crlDistPoint: String?, crlIssuer: X500Name? = null): X509Certificate { + val signatureScheme = Crypto.findSignatureScheme(issuerKeyPair.private) + val provider = Crypto.findProvider(signatureScheme.providerName) + val issuerSigner = ContentSignerBuilder.build(signatureScheme, issuerKeyPair.private, provider) + val builder = X509Utilities.createPartialCertificate( + certType, + currentCaCert.issuerX500Principal, + issuerKeyPair.public, + currentCaCert.subjectX500Principal, + currentCaCert.publicKey, + Pair(Date(System.currentTimeMillis() - 5.minutes.toMillis()), Date(System.currentTimeMillis() + 10.days.toMillis())), + null + ) + crlDistPoint?.let { + val distPointName = DistributionPointName(GeneralNames(GeneralName(GeneralName.uniformResourceIdentifier, it))) + val crlIssuerGeneralNames = crlIssuer?.let { + GeneralNames(GeneralName(crlIssuer)) + } + val distPoint = DistributionPoint(distPointName, null, crlIssuerGeneralNames) + builder.addExtension(Extension.cRLDistributionPoints, false, CRLDistPoint(arrayOf(distPoint))) + } + return builder.build(issuerSigner).toJca() + } + + @Path("crl") + inner class CrlServlet(private val server: CrlServer) { + + private val SIGNATURE_ALGORITHM = "SHA256withECDSA" + private val NODE_CRL = "node.crl" + private val INTEMEDIATE_CRL = "intermediate.crl" + private val EMPTY_CRL = "empty.crl" + + @GET + @Path("node.crl") + @Produces("application/pkcs7-crl") + fun getNodeCRL(): Response { + return Response.ok(createRevocationList( + INTERMEDIATE_CA.certificate, + INTERMEDIATE_CA.keyPair.private, + NODE_CRL, + false, + *revokedNodeCerts.toTypedArray()).encoded).build() + } + + @GET + @Path("intermediate.crl") + @Produces("application/pkcs7-crl") + fun getIntermediateCRL(): Response { + return Response.ok(createRevocationList( + ROOT_CA.certificate, + ROOT_CA.keyPair.private, + INTEMEDIATE_CRL, + false, + *revokedIntermediateCerts.toTypedArray()).encoded).build() + } + + @GET + @Path("empty.crl") + @Produces("application/pkcs7-crl") + fun getEmptyCRL(): Response { + return Response.ok(createRevocationList( + ROOT_CA.certificate, + ROOT_CA.keyPair.private, + EMPTY_CRL, true).encoded).build() + } + + private fun createRevocationList(caCertificate: X509Certificate, + caPrivateKey: PrivateKey, + endpoint: String, + indirect: Boolean, + vararg serialNumbers: BigInteger): X509CRL { + println("Generating CRL for $endpoint") + val builder = JcaX509v2CRLBuilder(caCertificate.subjectX500Principal, Date(System.currentTimeMillis() - 1.minutes.toMillis())) + val extensionUtils = JcaX509ExtensionUtils() + builder.addExtension(Extension.authorityKeyIdentifier, + false, extensionUtils.createAuthorityKeyIdentifier(caCertificate)) + val issuingDistPointName = GeneralName( + GeneralName.uniformResourceIdentifier, + "http://${server.hostAndPort.host}:${server.hostAndPort.port}/crl/$endpoint") + // This is required and needs to match the certificate settings with respect to being indirect + val issuingDistPoint = IssuingDistributionPoint(DistributionPointName(GeneralNames(issuingDistPointName)), indirect, false) + builder.addExtension(Extension.issuingDistributionPoint, true, issuingDistPoint) + builder.setNextUpdate(Date(System.currentTimeMillis() + 1.seconds.toMillis())) + serialNumbers.forEach { + builder.addCRLEntry(it, Date(System.currentTimeMillis() - 10.minutes.toMillis()), ReasonFlags.certificateHold) + } + val signer = JcaContentSignerBuilder(SIGNATURE_ALGORITHM).setProvider(BouncyCastleProvider.PROVIDER_NAME).build(caPrivateKey) + return JcaX509CRLConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCRL(builder.build(signer)) + } + } + + inner class CrlServer(hostAndPort: NetworkHostAndPort) : Closeable { + + private val server: Server = Server(InetSocketAddress(hostAndPort.host, hostAndPort.port)).apply { + handler = HandlerCollection().apply { + addHandler(buildServletContextHandler()) + } + } + + val hostAndPort: NetworkHostAndPort + get() = server.connectors.mapNotNull { it as? ServerConnector } + .map { NetworkHostAndPort(it.host, it.localPort) } + .first() + + override fun close() { + println("Shutting down network management web services...") + server.stop() + server.join() + } + + fun start() { + server.start() + println("Network management web services started on $hostAndPort") + } + + private fun buildServletContextHandler(): ServletContextHandler { + val crlServer = this + return ServletContextHandler().apply { + contextPath = "/" + val resourceConfig = ResourceConfig().apply { + register(CrlServlet(crlServer)) + } + val jerseyServlet = ServletHolder(ServletContainer(resourceConfig)).apply { initOrder = 0 } + addServlet(jerseyServlet, "/*") + } + } + } +} diff --git a/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt b/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt index bbac2dcb48..b8c857846e 100644 --- a/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt @@ -224,6 +224,7 @@ class ProtonWrapperTests { doReturn(NetworkHostAndPort("0.0.0.0", artemisPort)).whenever(it).p2pAddress doReturn(null).whenever(it).jmxMonitoringHttpPort doReturn(emptyList()).whenever(it).certificateChainCheckPolicies + doReturn(true).whenever(it).crlCheckSoftFail } artemisConfig.configureWithDevSSLCertificate() @@ -240,6 +241,7 @@ class ProtonWrapperTests { doReturn(BOB_NAME).whenever(it).myLegalName doReturn("trustpass").whenever(it).trustStorePassword doReturn("cordacadevpass").whenever(it).keyStorePassword + doReturn(true).whenever(it).crlCheckSoftFail } clientConfig.configureWithDevSSLCertificate() @@ -247,14 +249,15 @@ class ProtonWrapperTests { val clientKeystore = clientConfig.loadSslKeyStore().internal return AMQPClient( listOf(NetworkHostAndPort("localhost", serverPort), - NetworkHostAndPort("localhost", serverPort2), - NetworkHostAndPort("localhost", artemisPort)), + NetworkHostAndPort("localhost", serverPort2), + NetworkHostAndPort("localhost", artemisPort)), setOf(ALICE_NAME, CHARLIE_NAME), PEER_USER, PEER_USER, clientKeystore, clientConfig.keyStorePassword, - clientTruststore, true) + clientTruststore, + true) } private fun createSharedThreadsClient(sharedEventGroup: EventLoopGroup, id: Int): AMQPClient { @@ -263,6 +266,7 @@ class ProtonWrapperTests { doReturn(CordaX500Name(null, "client $id", "Corda", "London", null, "GB")).whenever(it).myLegalName doReturn("trustpass").whenever(it).trustStorePassword doReturn("cordacadevpass").whenever(it).keyStorePassword + doReturn(true).whenever(it).crlCheckSoftFail } clientConfig.configureWithDevSSLCertificate() @@ -275,7 +279,9 @@ class ProtonWrapperTests { PEER_USER, clientKeystore, clientConfig.keyStorePassword, - clientTruststore, true, sharedEventGroup) + clientTruststore, + true, + sharedThreadPool = sharedEventGroup) } private fun createServer(port: Int, name: CordaX500Name = ALICE_NAME): AMQPServer { @@ -284,6 +290,7 @@ class ProtonWrapperTests { doReturn(name).whenever(it).myLegalName doReturn("trustpass").whenever(it).trustStorePassword doReturn("cordacadevpass").whenever(it).keyStorePassword + doReturn(true).whenever(it).crlCheckSoftFail } serverConfig.configureWithDevSSLCertificate() @@ -296,6 +303,7 @@ class ProtonWrapperTests { PEER_USER, serverKeystore, serverConfig.keyStorePassword, - serverTruststore) + serverTruststore, + crlCheckSoftFail = true) } } diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt index 97e05787f9..3048be93bf 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt @@ -88,6 +88,7 @@ class MQSecurityAsNodeTest : P2PMQSecurityTest() { override val certificatesDirectory = Files.createTempDirectory("certs") override val keyStorePassword: String get() = "cordacadevpass" override val trustStorePassword: String get() = "trustpass" + override val crlCheckSoftFail: Boolean = true init { val legalName = CordaX500Name("MegaCorp", "London", "GB") diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt index 3793e96f94..feadd1ecee 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt @@ -89,6 +89,7 @@ data class NotaryConfig(val validating: Boolean, "raft, bftSMaRt, and custom configs cannot be specified together" } } + val isClusterConfig: Boolean get() = raft != null || bftSMaRt != null } @@ -128,10 +129,11 @@ data class NodeConfigurationImpl( override val emailAddress: String, override val keyStorePassword: String, override val trustStorePassword: String, + override val crlCheckSoftFail: Boolean, override val dataSourceProperties: Properties, override val compatibilityZoneURL: URL? = null, override val rpcUsers: List, - override val security : SecurityConfiguration? = null, + override val security: SecurityConfiguration? = null, override val verifierType: VerifierType, override val p2pMessagingRetry: P2PMessagingRetryConfiguration, override val p2pAddress: NetworkHostAndPort, @@ -155,15 +157,15 @@ data class NodeConfigurationImpl( override val attachmentCacheBound: Long = NodeConfiguration.defaultAttachmentCacheBound, override val extraNetworkMapKeys: List = emptyList(), // do not use or remove (breaks DemoBench together with rejection of unknown configuration keys during parsing) - private val h2port: Int = 0, + private val h2port: Int = 0, // do not use or remove (used by Capsule) private val jarDirs: List = emptyList() - ) : NodeConfiguration { +) : NodeConfiguration { companion object { private val logger = loggerFor() } - override val rpcOptions: NodeRpcOptions = initialiseRpcOptions(rpcAddress, rpcSettings, SslOptions(baseDirectory / "certificates", keyStorePassword, trustStorePassword)) + override val rpcOptions: NodeRpcOptions = initialiseRpcOptions(rpcAddress, rpcSettings, SslOptions(baseDirectory / "certificates", keyStorePassword, trustStorePassword, crlCheckSoftFail)) private fun initialiseRpcOptions(explicitAddress: NetworkHostAndPort?, settings: NodeRpcSettings, fallbackSslOptions: SSLConfiguration): NodeRpcOptions { return when { @@ -321,8 +323,8 @@ data class SecurityConfiguration(val authService: SecurityConfiguration.AuthServ } } - fun copyWithAdditionalUser(user: User) : DataSource{ - val extendedList = this.users?.toMutableList()?: mutableListOf() + fun copyWithAdditionalUser(user: User): DataSource { + val extendedList = this.users?.toMutableList() ?: mutableListOf() extendedList.add(user) return DataSource(this.type, this.passwordEncryption, this.connection, listOf(*extendedList.toTypedArray())) } diff --git a/node/src/main/kotlin/net/corda/node/services/config/SslOptions.kt b/node/src/main/kotlin/net/corda/node/services/config/SslOptions.kt index 6f1fd1941e..2d81045835 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/SslOptions.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/SslOptions.kt @@ -4,9 +4,20 @@ import net.corda.nodeapi.internal.config.SSLConfiguration import java.nio.file.Path import java.nio.file.Paths -data class SslOptions(override val certificatesDirectory: Path, override val keyStorePassword: String, override val trustStorePassword: String) : SSLConfiguration { +// TODO: we use both SSL and Ssl for names. We should pick one of them, or even better change to TLS +data class SslOptions(override val certificatesDirectory: Path, + override val keyStorePassword: String, + override val trustStorePassword: String, + override val crlCheckSoftFail: Boolean) : SSLConfiguration { - fun copy(certificatesDirectory: String = this.certificatesDirectory.toString(), keyStorePassword: String = this.keyStorePassword, trustStorePassword: String = this.trustStorePassword): SslOptions = copy(certificatesDirectory = certificatesDirectory.toAbsolutePath(), keyStorePassword = keyStorePassword, trustStorePassword = trustStorePassword) + fun copy(certificatesDirectory: String = this.certificatesDirectory.toString(), + keyStorePassword: String = this.keyStorePassword, + trustStorePassword: String = this.trustStorePassword, + crlCheckSoftFail: Boolean = this.crlCheckSoftFail): SslOptions = copy( + certificatesDirectory = certificatesDirectory.toAbsolutePath(), + keyStorePassword = keyStorePassword, + trustStorePassword = trustStorePassword, + crlCheckSoftFail = crlCheckSoftFail) } private fun String.toAbsolutePath() = Paths.get(this).toAbsolutePath() \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/config/shell/ShellConfig.kt b/node/src/main/kotlin/net/corda/node/services/config/shell/ShellConfig.kt index b4e421a884..453a9c7dbd 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/shell/ShellConfig.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/shell/ShellConfig.kt @@ -4,7 +4,6 @@ import net.corda.core.internal.div import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.services.Permissions import net.corda.node.services.config.NodeConfiguration -import net.corda.node.services.config.shouldInitCrashShell import net.corda.nodeapi.internal.config.User import net.corda.tools.shell.ShellConfiguration import net.corda.tools.shell.ShellConfiguration.Companion.COMMANDS_DIR @@ -22,7 +21,8 @@ fun NodeConfiguration.toShellConfig(): ShellConfiguration { ShellSslOptions(sslKeystore, keyStorePassword, trustStoreFile, - trustStorePassword) + trustStorePassword, + crlCheckSoftFail) } } else { null diff --git a/node/src/main/resources/reference.conf b/node/src/main/resources/reference.conf index 108e0fdc9c..9608bb1dc5 100644 --- a/node/src/main/resources/reference.conf +++ b/node/src/main/resources/reference.conf @@ -2,6 +2,7 @@ myLegalName = "Vast Global MegaCorp, Ltd" emailAddress = "admin@company.com" keyStorePassword = "cordacadevpass" trustStorePassword = "trustpass" +crlCheckSoftFail = true dataSourceProperties = { dataSourceClassName = org.h2.jdbcx.JdbcDataSource dataSource.url = "jdbc:h2:file:"${baseDirectory}"/persistence;DB_CLOSE_ON_EXIT=FALSE;LOCK_TIMEOUT=10000;WRITE_DELAY=100;AUTO_SERVER_PORT="${h2port} diff --git a/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt b/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt index d50b95c923..347fdddff4 100644 --- a/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt @@ -62,7 +62,7 @@ class NodeConfigurationImplTest { adminAddress = NetworkHostAndPort("localhost", 2), standAloneBroker = false, useSsl = false, - ssl = SslOptions(baseDirectory / "certificates", keyStorePassword, trustStorePassword)) + ssl = SslOptions(baseDirectory / "certificates", keyStorePassword, trustStorePassword, true)) return NodeConfigurationImpl( baseDirectory = baseDirectory, myLegalName = ALICE_NAME, @@ -79,7 +79,8 @@ class NodeConfigurationImplTest { certificateChainCheckPolicies = emptyList(), devMode = true, noLocalShell = false, - rpcSettings = rpcSettings + rpcSettings = rpcSettings, + crlCheckSoftFail = true ) } } diff --git a/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/UnsafeCertificatesFactory.kt b/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/UnsafeCertificatesFactory.kt index 7ef6ec7a3d..4ce15b78af 100644 --- a/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/UnsafeCertificatesFactory.kt +++ b/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/UnsafeCertificatesFactory.kt @@ -76,9 +76,13 @@ class KeyStores(val keyStore: UnsafeKeyStore, val trustStore: UnsafeKeyStore) { } } } - data class TestSslOptions(override val certificatesDirectory: Path, override val keyStorePassword: String, override val trustStorePassword: String) : SSLConfiguration - private fun sslConfiguration(directory: Path) = TestSslOptions(directory, keyStore.password, trustStore.password) + data class TestSslOptions(override val certificatesDirectory: Path, + override val keyStorePassword: String, + override val trustStorePassword: String, + override val crlCheckSoftFail: Boolean) : SSLConfiguration + + private fun sslConfiguration(directory: Path) = TestSslOptions(directory, keyStore.password, trustStore.password, true) } interface AutoClosableSSLConfiguration : AutoCloseable { @@ -121,6 +125,7 @@ data class UnsafeKeyStore(private val delegate: KeyStore, val password: String) class TemporaryFile(fileName: String, val directory: Path) : AutoCloseable { val file: Path = (directory / fileName).createFile().toAbsolutePath() + init { file.toFile().deleteOnExit() } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt index 72e4b5a9ee..c620ee6731 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt @@ -65,6 +65,7 @@ fun configureTestSSL(legalName: CordaX500Name): SSLConfiguration { override val certificatesDirectory = Files.createTempDirectory("certs") override val keyStorePassword: String get() = "cordacadevpass" override val trustStorePassword: String get() = "trustpass" + override val crlCheckSoftFail: Boolean = true init { configureDevKeyAndTrustStores(legalName) @@ -120,22 +121,24 @@ fun createDevNodeCaCertPath( /** Application of [doAnswer] that gets a value from the given [map] using the arg at [argIndex] as key. */ fun doLookup(map: Map<*, *>, argIndex: Int = 0) = doAnswer { map[it.arguments[argIndex]] } -fun SSLConfiguration.useSslRpcOverrides(): Map { +fun SSLConfiguration.useSslRpcOverrides(): Map { return mapOf( "rpcSettings.useSsl" to "true", "rpcSettings.ssl.certificatesDirectory" to certificatesDirectory.toString(), "rpcSettings.ssl.keyStorePassword" to keyStorePassword, - "rpcSettings.ssl.trustStorePassword" to trustStorePassword + "rpcSettings.ssl.trustStorePassword" to trustStorePassword, + "rpcSettings.ssl.crlCheckSoftFail" to true ) } -fun SSLConfiguration.noSslRpcOverrides(rpcAdminAddress: NetworkHostAndPort): Map { +fun SSLConfiguration.noSslRpcOverrides(rpcAdminAddress: NetworkHostAndPort): Map { return mapOf( "rpcSettings.adminAddress" to rpcAdminAddress.toString(), "rpcSettings.useSsl" to "false", "rpcSettings.ssl.certificatesDirectory" to certificatesDirectory.toString(), "rpcSettings.ssl.keyStorePassword" to keyStorePassword, - "rpcSettings.ssl.trustStorePassword" to trustStorePassword + "rpcSettings.ssl.trustStorePassword" to trustStorePassword, + "rpcSettings.ssl.crlCheckSoftFail" to true ) } diff --git a/tools/shell/src/integration-test/kotlin/net/corda/tools/shell/InteractiveShellIntegrationTest.kt b/tools/shell/src/integration-test/kotlin/net/corda/tools/shell/InteractiveShellIntegrationTest.kt index c8dc67869e..154fc21b14 100644 --- a/tools/shell/src/integration-test/kotlin/net/corda/tools/shell/InteractiveShellIntegrationTest.kt +++ b/tools/shell/src/integration-test/kotlin/net/corda/tools/shell/InteractiveShellIntegrationTest.kt @@ -80,7 +80,7 @@ class InteractiveShellIntegrationTest { startNode(rpcUsers = listOf(user), customOverrides = nodeSslOptions.useSslRpcOverrides()).getOrThrow().use { node -> val sslConfiguration = ShellSslOptions(clientSslOptions.sslKeystore, clientSslOptions.keyStorePassword, - clientSslOptions.trustStoreFile, clientSslOptions.trustStorePassword) + clientSslOptions.trustStoreFile, clientSslOptions.trustStorePassword, clientSslOptions.crlCheckSoftFail) val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(), user = user.username, password = user.password, hostAndPort = node.rpcAddress, @@ -117,7 +117,7 @@ class InteractiveShellIntegrationTest { startNode(rpcUsers = listOf(user), customOverrides = nodeSslOptions.useSslRpcOverrides()).getOrThrow().use { node -> val sslConfiguration = ShellSslOptions(clientSslOptions.sslKeystore, clientSslOptions.keyStorePassword, - clientSslOptions.trustStoreFile, clientSslOptions.trustStorePassword) + clientSslOptions.trustStoreFile, clientSslOptions.trustStorePassword, clientSslOptions.crlCheckSoftFail) val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(), user = user.username, password = user.password, hostAndPort = node.rpcAddress, @@ -199,7 +199,7 @@ class InteractiveShellIntegrationTest { startNode(rpcUsers = listOf(user), customOverrides = nodeSslOptions.useSslRpcOverrides()).getOrThrow().use { node -> val sslConfiguration = ShellSslOptions(clientSslOptions.sslKeystore, clientSslOptions.keyStorePassword, - clientSslOptions.trustStoreFile, clientSslOptions.trustStorePassword) + clientSslOptions.trustStoreFile, clientSslOptions.trustStorePassword, clientSslOptions.crlCheckSoftFail) val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(), user = user.username, password = user.password, hostAndPort = node.rpcAddress, diff --git a/tools/shell/src/main/kotlin/net/corda/tools/shell/ShellConfiguration.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/ShellConfiguration.kt index 2714679720..28b773b085 100644 --- a/tools/shell/src/main/kotlin/net/corda/tools/shell/ShellConfiguration.kt +++ b/tools/shell/src/main/kotlin/net/corda/tools/shell/ShellConfiguration.kt @@ -23,6 +23,12 @@ data class ShellConfiguration( } } -data class ShellSslOptions(override val sslKeystore: Path, override val keyStorePassword: String, override val trustStoreFile:Path, override val trustStorePassword: String) : SSLConfiguration { +//TODO: sslKeystore -> it's a path not the keystore itself. +//TODO: trustStoreFile -> it's a path not the file itself. +data class ShellSslOptions(override val sslKeystore: Path, + override val keyStorePassword: String, + override val trustStoreFile: Path, + override val trustStorePassword: String, + override val crlCheckSoftFail: Boolean) : SSLConfiguration { override val certificatesDirectory: Path get() = Paths.get("") } \ No newline at end of file diff --git a/tools/shell/src/main/kotlin/net/corda/tools/shell/StandaloneShellArgsParser.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/StandaloneShellArgsParser.kt index 736b922afc..29d2d9f7fe 100644 --- a/tools/shell/src/main/kotlin/net/corda/tools/shell/StandaloneShellArgsParser.kt +++ b/tools/shell/src/main/kotlin/net/corda/tools/shell/StandaloneShellArgsParser.kt @@ -27,10 +27,10 @@ class CommandLineOptionParser { .accepts("commands-directory", "The directory with additional CrAsH shell commands.") .withOptionalArg() private val hostArg = optionParser - .acceptsAll(listOf("h","host"), "The host of the Corda node.") + .acceptsAll(listOf("h", "host"), "The host of the Corda node.") .withRequiredArg() private val portArg = optionParser - .acceptsAll(listOf("p","port"), "The port of the Corda node.") + .acceptsAll(listOf("p", "port"), "The port of the Corda node.") .withRequiredArg() private val userArg = optionParser .accepts("user", "The RPC user name.") @@ -209,11 +209,13 @@ private class ShellConfigurationFile { sslKeystore = Paths.get(it.keystore.path), keyStorePassword = it.keystore.password, trustStoreFile = Paths.get(it.truststore.path), - trustStorePassword = it.truststore.password) + trustStorePassword = it.truststore.password, + crlCheckSoftFail = true) } return ShellConfiguration( - commandsDirectory = extensions?.commands?.let { Paths.get(it.path) } ?: Paths.get(".") / COMMANDS_DIR, + commandsDirectory = extensions?.commands?.let { Paths.get(it.path) } ?: Paths.get(".") + / COMMANDS_DIR, cordappsDirectory = extensions?.cordapps?.let { Paths.get(it.path) }, user = node.user ?: "", password = node.password ?: "", diff --git a/tools/shell/src/test/kotlin/net/corda/tools/shell/StandaloneShellArgsParserTest.kt b/tools/shell/src/test/kotlin/net/corda/tools/shell/StandaloneShellArgsParserTest.kt index 411d7230a3..9e5c896f92 100644 --- a/tools/shell/src/test/kotlin/net/corda/tools/shell/StandaloneShellArgsParserTest.kt +++ b/tools/shell/src/test/kotlin/net/corda/tools/shell/StandaloneShellArgsParserTest.kt @@ -102,12 +102,13 @@ class StandaloneShellArgsParserTest { trustStoreFile = Paths.get("/x/y/truststore.jks"), keyStoreType = "dummy", trustStoreType = "dummy" - ) + ) val expectedSsl = ShellSslOptions(sslKeystore = Paths.get("/x/y/keystore.jks"), keyStorePassword = "pass1", trustStoreFile = Paths.get("/x/y/truststore.jks"), - trustStorePassword = "pass2") + trustStorePassword = "pass2", + crlCheckSoftFail = true) val expectedConfig = ShellConfiguration( commandsDirectory = Paths.get("/x/y/commands"), cordappsDirectory = Paths.get("/x/y/cordapps"), @@ -148,7 +149,8 @@ class StandaloneShellArgsParserTest { val expectedSsl = ShellSslOptions(sslKeystore = Paths.get("/x/y/keystore.jks"), keyStorePassword = "pass1", trustStoreFile = Paths.get("/x/y/truststore.jks"), - trustStorePassword = "pass2") + trustStorePassword = "pass2", + crlCheckSoftFail = true) val expectedConfig = ShellConfiguration( commandsDirectory = Paths.get("/x/y/commands"), cordappsDirectory = Paths.get("/x/y/cordapps"), @@ -187,7 +189,8 @@ class StandaloneShellArgsParserTest { val expectedSsl = ShellSslOptions(sslKeystore = Paths.get("/x/y/cmd.jks"), keyStorePassword = "pass1", trustStoreFile = Paths.get("/x/y/truststore.jks"), - trustStorePassword = "pass2") + trustStorePassword = "pass2", + crlCheckSoftFail = true) val expectedConfig = ShellConfiguration( commandsDirectory = Paths.get("/x/y/commands"), cordappsDirectory = Paths.get("/x/y/cordapps"), diff --git a/webserver/src/main/kotlin/net/corda/webserver/WebServerConfig.kt b/webserver/src/main/kotlin/net/corda/webserver/WebServerConfig.kt index 28a960a224..793e6b4308 100644 --- a/webserver/src/main/kotlin/net/corda/webserver/WebServerConfig.kt +++ b/webserver/src/main/kotlin/net/corda/webserver/WebServerConfig.kt @@ -14,6 +14,7 @@ import java.nio.file.Path class WebServerConfig(override val baseDirectory: Path, val config: Config) : NodeSSLConfiguration { override val keyStorePassword: String by config override val trustStorePassword: String by config + override val crlCheckSoftFail: Boolean by config val useHTTPS: Boolean by config val myLegalName: String by config val rpcAddress: NetworkHostAndPort by lazy {