diff --git a/docs/source/corda-configuration-file.rst b/docs/source/corda-configuration-file.rst index 399b0bae78..7af075efed 100644 --- a/docs/source/corda-configuration-file.rst +++ b/docs/source/corda-configuration-file.rst @@ -239,6 +239,12 @@ absolute path to the node's base directory. .. _Dropwizard: https://metrics.dropwizard.io/3.2.3/manual/third-party.html .. _Introduction to New Relic for Java: https://docs.newrelic.com/docs/agents/java-agent/getting-started/introduction-new-relic-java +:cordappSignerKeyFingerprintBlacklist: List of public keys fingerprints (SHA-256 of public key hash) not allowed as Cordapp JARs signers. + Node will not load Cordapps signed by those keys. + The option takes effect only in production mode and defaults to Corda development keys (``["56CA54E803CB87C8472EBD3FBC6A2F1876E814CEEBF74860BD46997F40729367", + "83088052AF16700457AE2C978A7D8AC38DD6A7C713539D00B897CD03A5E5D31D"]``), in development mode any key is allowed to sign Cordpapp JARs. + + Examples -------- diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/KeyStoreConfigHelpers.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/KeyStoreConfigHelpers.kt index d6cb0f5877..b35ea6a762 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/KeyStoreConfigHelpers.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/KeyStoreConfigHelpers.kt @@ -2,8 +2,11 @@ package net.corda.nodeapi.internal import net.corda.core.crypto.Crypto import net.corda.core.crypto.Crypto.generateKeyPair +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.sha256 import net.corda.core.identity.CordaX500Name import net.corda.core.identity.PartyAndCertificate +import net.corda.core.internal.hash import net.corda.core.internal.toX500Name import net.corda.nodeapi.internal.config.CertificateStore import net.corda.nodeapi.internal.crypto.* @@ -99,7 +102,7 @@ const val DEV_CA_TRUST_STORE_FILE: String = "cordatruststore.jks" const val DEV_CA_TRUST_STORE_PASS: String = "trustpass" const val DEV_CA_TRUST_STORE_PRIVATE_KEY_PASS: String = "trustpasskeypass" -val DEV_CERTIFICATES: List get() = listOf(DEV_INTERMEDIATE_CA.certificate, DEV_ROOT_CA.certificate) +val DEV_PUB_KEY_HASHES: List get() = listOf(DEV_INTERMEDIATE_CA.certificate, DEV_ROOT_CA.certificate).map { it.publicKey.hash.sha256() } // We need a class so that we can get hold of the class loader internal object DevCaHelper { 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 aa6b5a16b8..af74587618 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -9,6 +9,7 @@ import net.corda.confidential.SwapIdentitiesHandler import net.corda.core.CordaException import net.corda.core.concurrent.CordaFuture import net.corda.core.context.InvocationContext +import net.corda.core.crypto.SecureHash import net.corda.core.crypto.newSecureRandom import net.corda.core.crypto.sign import net.corda.core.flows.* @@ -48,7 +49,6 @@ import net.corda.node.services.FinalityHandler import net.corda.node.services.NotaryChangeHandler import net.corda.node.services.api.* import net.corda.node.services.config.NodeConfiguration -import net.corda.node.services.config.NotaryConfig import net.corda.node.services.config.configureWithDevSSLCertificate import net.corda.node.services.config.rpc.NodeRpcOptions import net.corda.node.services.config.shell.toShellConfig @@ -72,7 +72,6 @@ import net.corda.node.services.transactions.SimpleNotaryService import net.corda.node.services.upgrade.ContractUpgradeServiceImpl import net.corda.node.services.vault.NodeVaultService import net.corda.node.utilities.* -import net.corda.nodeapi.internal.DEV_CERTIFICATES import net.corda.nodeapi.internal.NodeInfoAndSigned import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.config.CertificateStore @@ -514,12 +513,20 @@ abstract class AbstractNode(val configuration: NodeConfiguration, // CorDapp will be generated. generatedCordapps += VirtualCordapp.generateSimpleNotaryCordapp(versionInfo) } - val blacklistedCerts = if (configuration.devMode) emptyList() else DEV_CERTIFICATES + val blacklistedKeys = if (configuration.devMode) emptyList() + else configuration.cordappSignerKeyFingerprintBlacklist.mapNotNull { + try { + SecureHash.parse(it) + } catch (e: IllegalArgumentException) { + log.error("Error while adding key fingerprint $it to cordappSignerKeyFingerprintBlacklist due to ${e.message}", e) + throw e + } + } return JarScanningCordappLoader.fromDirectories( configuration.cordappDirectories, versionInfo, extraCordapps = generatedCordapps, - blacklistedCerts = blacklistedCerts + signerKeyFingerprintBlacklist = blacklistedKeys ) } diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt index 9bd47c564a..c27df16f0f 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt @@ -26,7 +26,6 @@ import java.lang.reflect.Modifier import java.net.URL import java.net.URLClassLoader import java.nio.file.Path -import java.security.cert.X509Certificate import java.util.* import java.util.jar.JarInputStream import kotlin.reflect.KClass @@ -40,7 +39,7 @@ import kotlin.streams.toList class JarScanningCordappLoader private constructor(private val cordappJarPaths: List, private val versionInfo: VersionInfo = VersionInfo.UNKNOWN, extraCordapps: List, - private val blacklistedCordappSigners: List = emptyList()) : CordappLoaderTemplate() { + private val signerKeyFingerprintBlacklist: List = emptyList()) : CordappLoaderTemplate() { override val cordapps: List by lazy { loadCordapps() + extraCordapps @@ -67,10 +66,10 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: fun fromDirectories(cordappDirs: Collection, versionInfo: VersionInfo = VersionInfo.UNKNOWN, extraCordapps: List = emptyList(), - blacklistedCerts: List = emptyList()): JarScanningCordappLoader { + signerKeyFingerprintBlacklist: List = emptyList()): JarScanningCordappLoader { logger.info("Looking for CorDapps in ${cordappDirs.distinct().joinToString(", ", "[", "]")}") val paths = cordappDirs.distinct().flatMap(this::jarUrlsInDirectory).map { it.restricted() } - return JarScanningCordappLoader(paths, versionInfo, extraCordapps, blacklistedCerts) + return JarScanningCordappLoader(paths, versionInfo, extraCordapps, signerKeyFingerprintBlacklist) } /** @@ -78,9 +77,10 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: * * @param scanJars Uses the JAR URLs provided for classpath scanning and Cordapp detection. */ - fun fromJarUrls(scanJars: List, versionInfo: VersionInfo = VersionInfo.UNKNOWN, extraCordapps: List = emptyList(), blacklistedCerts: List = emptyList()): JarScanningCordappLoader { + fun fromJarUrls(scanJars: List, versionInfo: VersionInfo = VersionInfo.UNKNOWN, extraCordapps: List = emptyList(), + cordappsSignerKeyFingerprintBlacklist: List = emptyList()): JarScanningCordappLoader { val paths = scanJars.map { it.restricted() } - return JarScanningCordappLoader(paths, versionInfo, extraCordapps, blacklistedCerts) + return JarScanningCordappLoader(paths, versionInfo, extraCordapps, cordappsSignerKeyFingerprintBlacklist) } private fun URL.restricted(rootPackageName: String? = null) = RestrictedURL(this, rootPackageName) @@ -110,15 +110,16 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: } } .filter { - if (blacklistedCordappSigners.isEmpty()) { + if (signerKeyFingerprintBlacklist.isEmpty()) { true //Nothing blacklisted, no need to check } else { val certificates = it.jarPath.openStream().let(::JarInputStream).use(JarSignatureCollector::collectCertificates) - if (certificates.isEmpty() || (certificates - blacklistedCordappSigners).isNotEmpty()) + val blockedCertificates = certificates.filter { it.publicKey.hash.sha256() in signerKeyFingerprintBlacklist } + if (certificates.isEmpty() || (certificates - blockedCertificates).isNotEmpty()) true // Cordapp is not signed or it is signed by at least one non-blacklisted certificate else { logger.warn("Not loading CorDapp ${it.info.shortName} (${it.info.vendor}) as it is signed by development key(s) only: " + - "${certificates.intersect(blacklistedCordappSigners).map { it.publicKey }}.") + "${blockedCertificates.map { it.publicKey }}.") false } } 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 3073d7574a..13c8261dc9 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 @@ -13,6 +13,7 @@ import net.corda.node.services.config.rpc.NodeRpcOptions import net.corda.nodeapi.BrokerRpcSslOptions import net.corda.nodeapi.internal.config.* import net.corda.nodeapi.internal.persistence.DatabaseConfig +import net.corda.nodeapi.internal.DEV_PUB_KEY_HASHES import net.corda.tools.shell.SSHDConfiguration import org.slf4j.Logger import java.net.URL @@ -78,6 +79,8 @@ interface NodeConfiguration { val cordappDirectories: List val flowOverrides: FlowOverrideConfig? + val cordappSignerKeyFingerprintBlacklist: List + fun validate(): List companion object { @@ -215,7 +218,8 @@ data class NodeConfigurationImpl( override val flowMonitorSuspensionLoggingThresholdMillis: Duration = DEFAULT_FLOW_MONITOR_SUSPENSION_LOGGING_THRESHOLD_MILLIS, override val cordappDirectories: List = listOf(baseDirectory / CORDAPPS_DIR_NAME_DEFAULT), override val jmxReporterType: JmxReporterType? = JmxReporterType.JOLOKIA, - override val flowOverrides: FlowOverrideConfig? + override val flowOverrides: FlowOverrideConfig?, + override val cordappSignerKeyFingerprintBlacklist: List = DEV_PUB_KEY_HASHES.map { it.toString() } ) : NodeConfiguration { companion object { private val logger = loggerFor() diff --git a/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt b/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt index feb4e7b476..9c0c418ea7 100644 --- a/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt @@ -6,7 +6,7 @@ import net.corda.core.internal.packageName import net.corda.node.VersionInfo import net.corda.testing.node.internal.TestCordappDirectories import net.corda.testing.node.internal.cordappForPackages -import net.corda.nodeapi.internal.DEV_CERTIFICATES +import net.corda.nodeapi.internal.DEV_PUB_KEY_HASHES import org.assertj.core.api.Assertions.assertThat import org.junit.Test import java.nio.file.Paths @@ -147,21 +147,21 @@ class JarScanningCordappLoaderTest { @Test fun `cordapp classloader loads app signed by allowed certificate`() { val jar = JarScanningCordappLoaderTest::class.java.getResource("signed/signed-by-dev-key.jar")!! - val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), blacklistedCerts = emptyList()) + val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), cordappsSignerKeyFingerprintBlacklist = emptyList()) assertThat(loader.cordapps).hasSize(1) } @Test fun `cordapp classloader does not load app signed by blacklisted certificate`() { val jar = JarScanningCordappLoaderTest::class.java.getResource("signed/signed-by-dev-key.jar")!! - val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), blacklistedCerts = DEV_CERTIFICATES) + val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), cordappsSignerKeyFingerprintBlacklist = DEV_PUB_KEY_HASHES) assertThat(loader.cordapps).hasSize(0) } @Test fun `cordapp classloader loads app signed by both allowed and non-blacklisted certificate`() { val jar = JarScanningCordappLoaderTest::class.java.getResource("signed/signed-by-two-keys.jar")!! - val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), blacklistedCerts = DEV_CERTIFICATES) + val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), cordappsSignerKeyFingerprintBlacklist = DEV_PUB_KEY_HASHES) assertThat(loader.cordapps).hasSize(1) } }