diff --git a/.gitignore b/.gitignore index 08499b7950..4ee1d156c3 100644 --- a/.gitignore +++ b/.gitignore @@ -103,4 +103,7 @@ virtualenv/ # Files you may find useful to have in your working directory. PLAN NOTES -TODO \ No newline at end of file +TODO + +# gradle-dependx plugin +.dependx/ diff --git a/node/src/integration-test/kotlin/net/corda/node/NodeKeystoreCheckTest.kt b/node/src/integration-test/kotlin/net/corda/node/NodeKeystoreCheckTest.kt deleted file mode 100644 index eca03d81d2..0000000000 --- a/node/src/integration-test/kotlin/net/corda/node/NodeKeystoreCheckTest.kt +++ /dev/null @@ -1,62 +0,0 @@ -package net.corda.node - -import net.corda.core.crypto.Crypto -import net.corda.core.internal.div -import net.corda.core.utilities.getOrThrow -import net.corda.node.services.config.configureDevKeyAndTrustStores -import net.corda.nodeapi.internal.crypto.CertificateType -import net.corda.nodeapi.internal.crypto.X509Utilities -import net.corda.testing.core.ALICE_NAME -import net.corda.testing.driver.DriverParameters -import net.corda.testing.driver.driver -import net.corda.coretesting.internal.stubs.CertificateStoreStubs -import org.assertj.core.api.Assertions.assertThatThrownBy -import org.junit.Test -import javax.security.auth.x500.X500Principal - -class NodeKeystoreCheckTest { - @Test(timeout=300_000) - fun `starting node in non-dev mode with no key store`() { - driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = emptyList(), allowHibernateToManageAppSchema = false)) { - assertThatThrownBy { - startNode(customOverrides = mapOf("devMode" to false)).getOrThrow() - }.hasMessageContaining("One or more keyStores (identity or TLS) or trustStore not found.") - } - } - - @Test(timeout=300_000) - fun `node should throw exception if cert path does not chain to the trust root`() { - driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = emptyList(), allowHibernateToManageAppSchema = false)) { - // Create keystores. - val keystorePassword = "password" - val certificatesDirectory = baseDirectory(ALICE_NAME) / "certificates" - val signingCertStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory, keystorePassword) - val p2pSslConfig = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory, keyStorePassword = keystorePassword, trustStorePassword = keystorePassword) - - p2pSslConfig.configureDevKeyAndTrustStores(ALICE_NAME, signingCertStore, certificatesDirectory) - - // This should pass with correct keystore. - val node = startNode( - providedName = ALICE_NAME, - customOverrides = mapOf("devMode" to false, - "keyStorePassword" to keystorePassword, - "trustStorePassword" to keystorePassword) - ).getOrThrow() - node.stop() - - // Fiddle with node keystore. - signingCertStore.get().update { - // Self signed root. - val badRootKeyPair = Crypto.generateKeyPair() - val badRoot = X509Utilities.createSelfSignedCACertificate(X500Principal("O=Bad Root,L=Lodnon,C=GB"), badRootKeyPair) - val nodeCA = getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA, signingCertStore.entryPassword) - val badNodeCACert = X509Utilities.createCertificate(CertificateType.NODE_CA, badRoot, badRootKeyPair, ALICE_NAME.x500Principal, nodeCA.keyPair.public) - setPrivateKey(X509Utilities.CORDA_CLIENT_CA, nodeCA.keyPair.private, listOf(badNodeCACert, badRoot), signingCertStore.entryPassword) - } - - assertThatThrownBy { - startNode(providedName = ALICE_NAME, customOverrides = mapOf("devMode" to false)).getOrThrow() - }.hasMessage("Client CA certificate must chain to the trusted root.") - } - } -} diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 3c22064c9c..4398e3f492 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -94,7 +94,6 @@ import net.corda.node.services.api.VaultServiceInternal import net.corda.node.services.api.WritableTransactionStorage import net.corda.node.services.attachments.NodeAttachmentTrustCalculator import net.corda.node.services.config.NodeConfiguration -import net.corda.node.services.config.configureWithDevSSLCertificate import net.corda.node.services.config.rpc.NodeRpcOptions import net.corda.node.services.config.shell.determineUnsafeUsers import net.corda.node.services.config.shell.toShellConfig @@ -149,8 +148,6 @@ import net.corda.nodeapi.internal.cordapp.CordappLoader import net.corda.nodeapi.internal.crypto.CertificateType 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_CLIENT_TLS -import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA import net.corda.nodeapi.internal.crypto.X509Utilities.DEFAULT_VALIDITY_WINDOW import net.corda.nodeapi.internal.crypto.X509Utilities.DISTRIBUTED_NOTARY_COMPOSITE_KEY_ALIAS import net.corda.nodeapi.internal.crypto.X509Utilities.DISTRIBUTED_NOTARY_KEY_ALIAS @@ -176,10 +173,8 @@ import org.jolokia.jvmagent.JolokiaServer import org.jolokia.jvmagent.JolokiaServerConfig import org.slf4j.Logger import rx.Scheduler -import java.io.IOException import java.lang.reflect.InvocationTargetException import java.security.KeyPair -import java.security.KeyStoreException import java.security.cert.X509Certificate import java.sql.Connection import java.sql.Savepoint @@ -434,18 +429,6 @@ abstract class AbstractNode(val configuration: NodeConfiguration, return proxies.fold(ops) { delegate, decorate -> decorate(delegate) } } - private fun initKeyStores(): X509Certificate { - if (configuration.devMode) { - configuration.configureWithDevSSLCertificate(cryptoService) - // configureWithDevSSLCertificate is a devMode process that writes directly to keystore files, so - // we should re-synchronise BCCryptoService with the updated keystore file. - if (cryptoService is BCCryptoService) { - cryptoService.resyncKeystore() - } - } - return validateKeyStores() - } - private fun quasarExcludePackages(nodeConfiguration: NodeConfiguration) { val quasarInstrumentor = Retransform.getInstrumentor() @@ -457,7 +440,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, open fun generateAndSaveNodeInfo(): NodeInfo { check(started == null) { "Node has already been started" } log.info("Generating nodeInfo ...") - val trustRoot = initKeyStores() + val trustRoot = configuration.initKeyStores(cryptoService) startDatabase() val (identity, identityKeyPair) = obtainIdentity() val nodeCa = configuration.signingCertificateStore.get()[CORDA_CLIENT_CA] @@ -497,7 +480,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, logVendorString(database, log) if (allowHibernateToManageAppSchema) { Node.printBasicNodeInfo("Initialising CorDapps to get schemas created by hibernate") - val trustRoot = initKeyStores() + val trustRoot = configuration.initKeyStores(cryptoService) networkMapClient?.start(trustRoot) val (netParams, signedNetParams) = NetworkParametersReader(trustRoot, networkMapClient, configuration.baseDirectory).read() log.info("Loaded network parameters: $netParams") @@ -536,7 +519,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, nodeLifecycleEventsDistributor.distributeEvent(NodeLifecycleEvent.BeforeNodeStart(nodeServicesContext)) log.info("Node starting up ...") - val trustRoot = initKeyStores() + val trustRoot = configuration.initKeyStores(cryptoService) initialiseJolokia() schemaService.mappedSchemasWarnings().forEach { @@ -980,57 +963,6 @@ abstract class AbstractNode(val configuration: NodeConfiguration, @VisibleForTesting protected open fun acceptableLiveFiberCountOnStop(): Int = 0 - private fun getCertificateStores(): AllCertificateStores? { - return try { - // The following will throw IOException if key file not found or KeyStoreException if keystore password is incorrect. - val sslKeyStore = configuration.p2pSslOptions.keyStore.get() - val signingCertificateStore = configuration.signingCertificateStore.get() - val trustStore = configuration.p2pSslOptions.trustStore.get() - AllCertificateStores(trustStore, sslKeyStore, signingCertificateStore) - } catch (e: IOException) { - log.error("IO exception while trying to validate keystores and truststore", e) - null - } - } - - private data class AllCertificateStores(val trustStore: CertificateStore, val sslKeyStore: CertificateStore, val identitiesKeyStore: CertificateStore) - - private fun validateKeyStores(): X509Certificate { - // Step 1. Check trustStore, sslKeyStore and identitiesKeyStore exist. - val certStores = try { - requireNotNull(getCertificateStores()) { - "One or more keyStores (identity or TLS) or trustStore not found. " + - "Please either copy your existing keys and certificates from another node, " + - "or if you don't have one yet, fill out the config file and run corda.jar initial-registration." - } - } catch (e: KeyStoreException) { - throw IllegalArgumentException("At least one of the keystores or truststore passwords does not match configuration.") - } - // Step 2. Check that trustStore contains the correct key-alias entry. - require(CORDA_ROOT_CA in certStores.trustStore) { - "Alias for trustRoot key not found. Please ensure you have an updated trustStore file." - } - // Step 3. Check that tls keyStore contains the correct key-alias entry. - require(CORDA_CLIENT_TLS in certStores.sslKeyStore) { - "Alias for TLS key not found. Please ensure you have an updated TLS keyStore file." - } - - // Step 4. Check that identity keyStores contain the correct key-alias entry for Node CA. - require(CORDA_CLIENT_CA in certStores.identitiesKeyStore) { - "Alias for Node CA key not found. Please ensure you have an updated identity keyStore file." - } - - // Step 5. Check all cert paths chain to the trusted root. - val trustRoot = certStores.trustStore[CORDA_ROOT_CA] - val sslCertChainRoot = certStores.sslKeyStore.query { getCertificateChain(CORDA_CLIENT_TLS) }.last() - val nodeCaCertChainRoot = certStores.identitiesKeyStore.query { getCertificateChain(CORDA_CLIENT_CA) }.last() - - require(sslCertChainRoot == trustRoot) { "TLS certificate must chain to the trusted root." } - require(nodeCaCertChainRoot == trustRoot) { "Client CA certificate must chain to the trusted root." } - - return trustRoot - } - // Specific class so that MockNode can catch it. class DatabaseConfigurationException(message: String) : CordaException(message) diff --git a/node/src/main/kotlin/net/corda/node/internal/NodeKeyStoreUtilities.kt b/node/src/main/kotlin/net/corda/node/internal/NodeKeyStoreUtilities.kt new file mode 100644 index 0000000000..904c0baa20 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/NodeKeyStoreUtilities.kt @@ -0,0 +1,76 @@ +package net.corda.node.internal + +import net.corda.core.utilities.loggerFor +import net.corda.node.services.config.NodeConfiguration +import net.corda.node.services.config.configureWithDevSSLCertificate +import net.corda.nodeapi.internal.config.CertificateStore +import net.corda.nodeapi.internal.crypto.X509Utilities +import net.corda.nodeapi.internal.cryptoservice.CryptoService +import net.corda.nodeapi.internal.cryptoservice.bouncycastle.BCCryptoService +import java.io.IOException +import java.security.KeyStoreException +import java.security.cert.X509Certificate + +private data class AllCertificateStores(val trustStore: CertificateStore, val sslKeyStore: CertificateStore, val identitiesKeyStore: CertificateStore) + + +internal fun NodeConfiguration.initKeyStores(cryptoService: CryptoService): X509Certificate { + if (devMode) { + configureWithDevSSLCertificate(cryptoService) + // configureWithDevSSLCertificate is a devMode process that writes directly to keystore files, so + // we should re-synchronise BCCryptoService with the updated keystore file. + if (cryptoService is BCCryptoService) { + cryptoService.resyncKeystore() + } + } + return validateKeyStores() +} + +private fun NodeConfiguration.validateKeyStores(): X509Certificate { + // Step 1. Check trustStore, sslKeyStore and identitiesKeyStore exist. + val certStores = try { + requireNotNull(getCertificateStores()) { + "One or more keyStores (identity or TLS) or trustStore not found. " + + "Please either copy your existing keys and certificates from another node, " + + "or if you don't have one yet, fill out the config file and run corda.jar initial-registration." + } + } catch (e: KeyStoreException) { + throw IllegalArgumentException("At least one of the keystores or truststore passwords does not match configuration.") + } + // Step 2. Check that trustStore contains the correct key-alias entry. + require(X509Utilities.CORDA_ROOT_CA in certStores.trustStore) { + "Alias for trustRoot key not found. Please ensure you have an updated trustStore file." + } + // Step 3. Check that tls keyStore contains the correct key-alias entry. + require(X509Utilities.CORDA_CLIENT_TLS in certStores.sslKeyStore) { + "Alias for TLS key not found. Please ensure you have an updated TLS keyStore file." + } + + // Step 4. Check that identity keyStores contain the correct key-alias entry for Node CA. + require(X509Utilities.CORDA_CLIENT_CA in certStores.identitiesKeyStore) { + "Alias for Node CA key not found. Please ensure you have an updated identity keyStore file." + } + + // Step 5. Check all cert paths chain to the trusted root. + val trustRoot = certStores.trustStore[X509Utilities.CORDA_ROOT_CA] + val sslCertChainRoot = certStores.sslKeyStore.query { getCertificateChain(X509Utilities.CORDA_CLIENT_TLS) }.last() + val nodeCaCertChainRoot = certStores.identitiesKeyStore.query { getCertificateChain(X509Utilities.CORDA_CLIENT_CA) }.last() + + require(sslCertChainRoot == trustRoot) { "TLS certificate must chain to the trusted root." } + require(nodeCaCertChainRoot == trustRoot) { "Client CA certificate must chain to the trusted root." } + + return trustRoot +} + +private fun NodeConfiguration.getCertificateStores(): AllCertificateStores? { + return try { + // The following will throw IOException if key file not found or KeyStoreException if keystore password is incorrect. + val sslKeyStore = p2pSslOptions.keyStore.get() + val signingCertificateStore = signingCertificateStore.get() + val trustStore = p2pSslOptions.trustStore.get() + AllCertificateStores(trustStore, sslKeyStore, signingCertificateStore) + } catch (e: IOException) { + loggerFor().error("IO exception while trying to validate keystores and truststore", e) + null + } +} diff --git a/node/src/test/kotlin/net/corda/node/internal/NodeKeyStoreUtilitiesTest.kt b/node/src/test/kotlin/net/corda/node/internal/NodeKeyStoreUtilitiesTest.kt new file mode 100644 index 0000000000..d5c96cd080 --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/internal/NodeKeyStoreUtilitiesTest.kt @@ -0,0 +1,157 @@ +package net.corda.node.internal + +import com.nhaarman.mockito_kotlin.any +import com.nhaarman.mockito_kotlin.doAnswer +import com.nhaarman.mockito_kotlin.mock +import com.nhaarman.mockito_kotlin.verify +import com.nhaarman.mockito_kotlin.whenever +import net.corda.node.services.config.NodeConfiguration +import net.corda.nodeapi.internal.config.CertificateStore +import net.corda.nodeapi.internal.config.FileBasedCertificateStoreSupplier +import net.corda.nodeapi.internal.config.MutualSslConfiguration +import net.corda.nodeapi.internal.crypto.X509KeyStore +import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_CA +import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_TLS +import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA +import net.corda.nodeapi.internal.cryptoservice.CryptoService +import net.corda.nodeapi.internal.cryptoservice.bouncycastle.BCCryptoService +import net.corda.testing.core.ALICE_NAME +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.Test +import java.io.IOException +import java.security.KeyStoreException +import java.security.cert.X509Certificate + +class NodeKeyStoreUtilitiesTest { + @Test(timeout = 300_000) + fun `initializing key store in non-dev mode with no key store`() { + whenever(signingSupplier.get()).doAnswer { throw IOException() } + + assertThatThrownBy { + config.initKeyStores(cryptoService) + }.hasMessageContaining("One or more keyStores (identity or TLS) or trustStore not found.") + } + + @Test(timeout = 300_000) + fun `initializing key store in non-dev mode with invalid password`() { + whenever(signingSupplier.get()).doAnswer { throw KeyStoreException() } + + assertThatThrownBy { + config.initKeyStores(cryptoService) + }.hasMessageContaining("At least one of the keystores or truststore passwords does not match configuration") + } + + @Test(timeout = 300_000) + fun `initializing key store in non-dev mode without trusted root`() { + whenever(trustStore.contains(CORDA_ROOT_CA)).thenReturn(false) + + assertThatThrownBy { + config.initKeyStores(cryptoService) + }.hasMessageContaining("Alias for trustRoot key not found. Please ensure you have an updated trustStore file") + } + + @Test(timeout = 300_000) + fun `initializing key store in non-dev mode without alias for TLS key`() { + whenever(keyStore.contains(CORDA_CLIENT_TLS)).thenReturn(false) + + assertThatThrownBy { + config.initKeyStores(cryptoService) + }.hasMessageContaining("Alias for TLS key not found. Please ensure you have an updated TLS keyStore file") + } + + @Test(timeout = 300_000) + fun `initializing key store in non-dev mode without alias for node CA key`() { + whenever(signingStore.contains(CORDA_CLIENT_CA)).thenReturn(false) + + assertThatThrownBy { + config.initKeyStores(cryptoService) + }.hasMessageContaining("Alias for Node CA key not found. Please ensure you have an updated identity keyStore file") + } + + @Test(timeout = 300_000) + fun `initializing key store should throw exception if cert path does not chain to the trust root`() { + val untrustedRoot = mock() + whenever(signingStore.query(any List>())).thenReturn(mutableListOf(untrustedRoot)) + + assertThatThrownBy { + config.initKeyStores(cryptoService) + }.hasMessageContaining("Client CA certificate must chain to the trusted root") + } + + @Test(timeout = 300_000) + fun `initializing key store should throw exception if TLS certificate does not chain to the trust root`() { + val untrustedRoot = mock() + whenever(keyStore.query(any List>())).thenReturn(mutableListOf(untrustedRoot)) + + assertThatThrownBy { + config.initKeyStores(cryptoService) + }.hasMessageContaining("TLS certificate must chain to the trusted root") + } + + @Test(timeout = 300_000) + fun `initializing key store should return valid certificate if certificate is valid`() { + val certificate = config.initKeyStores(cryptoService) + + assertThat(certificate).isEqualTo(trustRoot) + } + + @Test(timeout = 300_000) + fun `initializing key store in dev mode check te supplier`() { + whenever(config.devMode).thenReturn(true) + whenever(config.myLegalName).thenReturn(ALICE_NAME) + whenever(config.certificatesDirectory).thenReturn(mock()) + whenever(trustSupplier.getOptional()).thenReturn(mock()) + whenever(keySupplier.getOptional()).thenReturn(mock()) + whenever(signingSupplier.getOptional()).thenReturn(mock()) + + config.initKeyStores(cryptoService) + + verify(signingSupplier).getOptional() + } + + @Test(timeout = 300_000) + fun `initializing key store in dev mode with BCCryptoService call resyncKeystore`() { + val bCryptoService = mock() + whenever(config.devMode).thenReturn(true) + whenever(config.myLegalName).thenReturn(ALICE_NAME) + whenever(config.certificatesDirectory).thenReturn(mock()) + whenever(trustSupplier.getOptional()).thenReturn(mock()) + whenever(keySupplier.getOptional()).thenReturn(mock()) + whenever(signingSupplier.getOptional()).thenReturn(mock()) + + config.initKeyStores(bCryptoService) + + verify(bCryptoService).resyncKeystore() + } + + private val config = mock() + + private val trustStore = mock() + private val signingStore = mock() + private val keyStore = mock() + private val sslOptions = mock() + private val trustSupplier = mock() + private val signingSupplier = mock() + private val keySupplier = mock() + private val trustRoot = mock() + private val cryptoService = mock() + + init { + whenever(config.devMode).thenReturn(false) + + whenever(sslOptions.keyStore).thenReturn(keySupplier) + whenever(sslOptions.trustStore).thenReturn(trustSupplier) + whenever(config.signingCertificateStore).thenReturn(signingSupplier) + whenever(trustSupplier.get()).thenReturn(trustStore) + whenever(signingSupplier.get()).thenReturn(signingStore) + whenever(keySupplier.get()).thenReturn(keyStore) + whenever(trustStore.contains(CORDA_ROOT_CA)).thenReturn(true) + whenever(keyStore.contains(CORDA_CLIENT_TLS)).thenReturn(true) + whenever(signingStore.contains(CORDA_CLIENT_CA)).thenReturn(true) + whenever(config.p2pSslOptions).thenReturn(sslOptions) + whenever(trustStore[CORDA_ROOT_CA]).thenReturn(trustRoot) + whenever(signingStore.query(any List>())).thenReturn(mutableListOf(trustRoot)) + whenever(keyStore.query(any List>())).thenReturn(mutableListOf(trustRoot)) + } +}