Optionally allow the node in production mode to accept Cordapps signed by dev Key (CORDA-1915) (#4133)

By default Cordaps build by corda-gradle-plugins are signed by Corda development key.
In dev mode any key can be used to sign Cordapp JAR .
In production node Corda dev keys were forbidden. This code change allows to opt-out by setting node option cordappSignerKeyFingerprintBlacklist=[] or specify more public keys to blacklist.
The option is used in production only mode.
This commit is contained in:
szymonsztuka 2018-10-30 13:53:01 +00:00 committed by GitHub
parent 38078e3762
commit 1de56550b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 40 additions and 19 deletions

View File

@ -239,6 +239,12 @@ absolute path to the node's base directory.
.. _Dropwizard: https://metrics.dropwizard.io/3.2.3/manual/third-party.html .. _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 .. _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 Examples
-------- --------

View File

@ -2,8 +2,11 @@ package net.corda.nodeapi.internal
import net.corda.core.crypto.Crypto import net.corda.core.crypto.Crypto
import net.corda.core.crypto.Crypto.generateKeyPair 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.CordaX500Name
import net.corda.core.identity.PartyAndCertificate import net.corda.core.identity.PartyAndCertificate
import net.corda.core.internal.hash
import net.corda.core.internal.toX500Name import net.corda.core.internal.toX500Name
import net.corda.nodeapi.internal.config.CertificateStore import net.corda.nodeapi.internal.config.CertificateStore
import net.corda.nodeapi.internal.crypto.* 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_PASS: String = "trustpass"
const val DEV_CA_TRUST_STORE_PRIVATE_KEY_PASS: String = "trustpasskeypass" const val DEV_CA_TRUST_STORE_PRIVATE_KEY_PASS: String = "trustpasskeypass"
val DEV_CERTIFICATES: List<X509Certificate> get() = listOf(DEV_INTERMEDIATE_CA.certificate, DEV_ROOT_CA.certificate) val DEV_PUB_KEY_HASHES: List<SecureHash.SHA256> 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 // We need a class so that we can get hold of the class loader
internal object DevCaHelper { internal object DevCaHelper {

View File

@ -9,6 +9,7 @@ import net.corda.confidential.SwapIdentitiesHandler
import net.corda.core.CordaException import net.corda.core.CordaException
import net.corda.core.concurrent.CordaFuture import net.corda.core.concurrent.CordaFuture
import net.corda.core.context.InvocationContext import net.corda.core.context.InvocationContext
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.newSecureRandom import net.corda.core.crypto.newSecureRandom
import net.corda.core.crypto.sign import net.corda.core.crypto.sign
import net.corda.core.flows.* 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.NotaryChangeHandler
import net.corda.node.services.api.* import net.corda.node.services.api.*
import net.corda.node.services.config.NodeConfiguration 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.configureWithDevSSLCertificate
import net.corda.node.services.config.rpc.NodeRpcOptions import net.corda.node.services.config.rpc.NodeRpcOptions
import net.corda.node.services.config.shell.toShellConfig 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.upgrade.ContractUpgradeServiceImpl
import net.corda.node.services.vault.NodeVaultService import net.corda.node.services.vault.NodeVaultService
import net.corda.node.utilities.* import net.corda.node.utilities.*
import net.corda.nodeapi.internal.DEV_CERTIFICATES
import net.corda.nodeapi.internal.NodeInfoAndSigned import net.corda.nodeapi.internal.NodeInfoAndSigned
import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.SignedNodeInfo
import net.corda.nodeapi.internal.config.CertificateStore import net.corda.nodeapi.internal.config.CertificateStore
@ -514,12 +513,20 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
// CorDapp will be generated. // CorDapp will be generated.
generatedCordapps += VirtualCordapp.generateSimpleNotaryCordapp(versionInfo) 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( return JarScanningCordappLoader.fromDirectories(
configuration.cordappDirectories, configuration.cordappDirectories,
versionInfo, versionInfo,
extraCordapps = generatedCordapps, extraCordapps = generatedCordapps,
blacklistedCerts = blacklistedCerts signerKeyFingerprintBlacklist = blacklistedKeys
) )
} }

View File

@ -26,7 +26,6 @@ import java.lang.reflect.Modifier
import java.net.URL import java.net.URL
import java.net.URLClassLoader import java.net.URLClassLoader
import java.nio.file.Path import java.nio.file.Path
import java.security.cert.X509Certificate
import java.util.* import java.util.*
import java.util.jar.JarInputStream import java.util.jar.JarInputStream
import kotlin.reflect.KClass import kotlin.reflect.KClass
@ -40,7 +39,7 @@ import kotlin.streams.toList
class JarScanningCordappLoader private constructor(private val cordappJarPaths: List<RestrictedURL>, class JarScanningCordappLoader private constructor(private val cordappJarPaths: List<RestrictedURL>,
private val versionInfo: VersionInfo = VersionInfo.UNKNOWN, private val versionInfo: VersionInfo = VersionInfo.UNKNOWN,
extraCordapps: List<CordappImpl>, extraCordapps: List<CordappImpl>,
private val blacklistedCordappSigners: List<X509Certificate> = emptyList()) : CordappLoaderTemplate() { private val signerKeyFingerprintBlacklist: List<SecureHash.SHA256> = emptyList()) : CordappLoaderTemplate() {
override val cordapps: List<CordappImpl> by lazy { override val cordapps: List<CordappImpl> by lazy {
loadCordapps() + extraCordapps loadCordapps() + extraCordapps
@ -67,10 +66,10 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
fun fromDirectories(cordappDirs: Collection<Path>, fun fromDirectories(cordappDirs: Collection<Path>,
versionInfo: VersionInfo = VersionInfo.UNKNOWN, versionInfo: VersionInfo = VersionInfo.UNKNOWN,
extraCordapps: List<CordappImpl> = emptyList(), extraCordapps: List<CordappImpl> = emptyList(),
blacklistedCerts: List<X509Certificate> = emptyList()): JarScanningCordappLoader { signerKeyFingerprintBlacklist: List<SecureHash.SHA256> = emptyList()): JarScanningCordappLoader {
logger.info("Looking for CorDapps in ${cordappDirs.distinct().joinToString(", ", "[", "]")}") logger.info("Looking for CorDapps in ${cordappDirs.distinct().joinToString(", ", "[", "]")}")
val paths = cordappDirs.distinct().flatMap(this::jarUrlsInDirectory).map { it.restricted() } 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. * @param scanJars Uses the JAR URLs provided for classpath scanning and Cordapp detection.
*/ */
fun fromJarUrls(scanJars: List<URL>, versionInfo: VersionInfo = VersionInfo.UNKNOWN, extraCordapps: List<CordappImpl> = emptyList(), blacklistedCerts: List<X509Certificate> = emptyList()): JarScanningCordappLoader { fun fromJarUrls(scanJars: List<URL>, versionInfo: VersionInfo = VersionInfo.UNKNOWN, extraCordapps: List<CordappImpl> = emptyList(),
cordappsSignerKeyFingerprintBlacklist: List<SecureHash.SHA256> = emptyList()): JarScanningCordappLoader {
val paths = scanJars.map { it.restricted() } 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) private fun URL.restricted(rootPackageName: String? = null) = RestrictedURL(this, rootPackageName)
@ -110,15 +110,16 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
} }
} }
.filter { .filter {
if (blacklistedCordappSigners.isEmpty()) { if (signerKeyFingerprintBlacklist.isEmpty()) {
true //Nothing blacklisted, no need to check true //Nothing blacklisted, no need to check
} else { } else {
val certificates = it.jarPath.openStream().let(::JarInputStream).use(JarSignatureCollector::collectCertificates) 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 true // Cordapp is not signed or it is signed by at least one non-blacklisted certificate
else { else {
logger.warn("Not loading CorDapp ${it.info.shortName} (${it.info.vendor}) as it is signed by development key(s) only: " + 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 false
} }
} }

View File

@ -13,6 +13,7 @@ import net.corda.node.services.config.rpc.NodeRpcOptions
import net.corda.nodeapi.BrokerRpcSslOptions import net.corda.nodeapi.BrokerRpcSslOptions
import net.corda.nodeapi.internal.config.* import net.corda.nodeapi.internal.config.*
import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.nodeapi.internal.DEV_PUB_KEY_HASHES
import net.corda.tools.shell.SSHDConfiguration import net.corda.tools.shell.SSHDConfiguration
import org.slf4j.Logger import org.slf4j.Logger
import java.net.URL import java.net.URL
@ -78,6 +79,8 @@ interface NodeConfiguration {
val cordappDirectories: List<Path> val cordappDirectories: List<Path>
val flowOverrides: FlowOverrideConfig? val flowOverrides: FlowOverrideConfig?
val cordappSignerKeyFingerprintBlacklist: List<String>
fun validate(): List<String> fun validate(): List<String>
companion object { companion object {
@ -215,7 +218,8 @@ data class NodeConfigurationImpl(
override val flowMonitorSuspensionLoggingThresholdMillis: Duration = DEFAULT_FLOW_MONITOR_SUSPENSION_LOGGING_THRESHOLD_MILLIS, override val flowMonitorSuspensionLoggingThresholdMillis: Duration = DEFAULT_FLOW_MONITOR_SUSPENSION_LOGGING_THRESHOLD_MILLIS,
override val cordappDirectories: List<Path> = listOf(baseDirectory / CORDAPPS_DIR_NAME_DEFAULT), override val cordappDirectories: List<Path> = listOf(baseDirectory / CORDAPPS_DIR_NAME_DEFAULT),
override val jmxReporterType: JmxReporterType? = JmxReporterType.JOLOKIA, override val jmxReporterType: JmxReporterType? = JmxReporterType.JOLOKIA,
override val flowOverrides: FlowOverrideConfig? override val flowOverrides: FlowOverrideConfig?,
override val cordappSignerKeyFingerprintBlacklist: List<String> = DEV_PUB_KEY_HASHES.map { it.toString() }
) : NodeConfiguration { ) : NodeConfiguration {
companion object { companion object {
private val logger = loggerFor<NodeConfigurationImpl>() private val logger = loggerFor<NodeConfigurationImpl>()

View File

@ -6,7 +6,7 @@ import net.corda.core.internal.packageName
import net.corda.node.VersionInfo import net.corda.node.VersionInfo
import net.corda.testing.node.internal.TestCordappDirectories import net.corda.testing.node.internal.TestCordappDirectories
import net.corda.testing.node.internal.cordappForPackages 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.assertj.core.api.Assertions.assertThat
import org.junit.Test import org.junit.Test
import java.nio.file.Paths import java.nio.file.Paths
@ -147,21 +147,21 @@ class JarScanningCordappLoaderTest {
@Test @Test
fun `cordapp classloader loads app signed by allowed certificate`() { fun `cordapp classloader loads app signed by allowed certificate`() {
val jar = JarScanningCordappLoaderTest::class.java.getResource("signed/signed-by-dev-key.jar")!! 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) assertThat(loader.cordapps).hasSize(1)
} }
@Test @Test
fun `cordapp classloader does not load app signed by blacklisted certificate`() { fun `cordapp classloader does not load app signed by blacklisted certificate`() {
val jar = JarScanningCordappLoaderTest::class.java.getResource("signed/signed-by-dev-key.jar")!! 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) assertThat(loader.cordapps).hasSize(0)
} }
@Test @Test
fun `cordapp classloader loads app signed by both allowed and non-blacklisted certificate`() { 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 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) assertThat(loader.cordapps).hasSize(1)
} }
} }