diff --git a/core/src/main/kotlin/net/corda/core/internal/JarSignatureCollector.kt b/core/src/main/kotlin/net/corda/core/internal/JarSignatureCollector.kt index 963623e474..f7bece5b01 100644 --- a/core/src/main/kotlin/net/corda/core/internal/JarSignatureCollector.kt +++ b/core/src/main/kotlin/net/corda/core/internal/JarSignatureCollector.kt @@ -28,6 +28,14 @@ object JarSignatureCollector { fun collectSigningParties(jar: JarInputStream): List = getSigners(jar).toPartiesOrderedByName() + /** + * Returns an ordered list of every [X509Certificate] which has signed every signable item in the given [JarInputStream]. + * + * @param jar The open [JarInputStream] to collect signing parties from. + * @throws InvalidJarSignersException If the signer sets for any two signable items are different from each other. + */ + fun collectCertificates(jar: JarInputStream): List = getSigners(jar).toCertificates() + private fun getSigners(jar: JarInputStream): Set { val signerSets = jar.fileSignerSets if (signerSets.isEmpty()) return emptySet() @@ -71,6 +79,10 @@ object JarSignatureCollector { (it.signerCertPath.certificates[0] as X509Certificate).publicKey }.sortedBy { it.hash} // Sorted for determinism. + private fun Set.toCertificates(): List = map { + it.signerCertPath.certificates[0] as X509Certificate + }.sortedBy { it.toString() } // Sorted for determinism. + private val JarInputStream.entries get(): Sequence = generateSequence(nextJarEntry) { nextJarEntry } } 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 03caed3ca2..d6cb0f5877 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 @@ -99,6 +99,8 @@ 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) + // We need a class so that we can get hold of the class loader internal object DevCaHelper { fun loadDevCa(alias: String): CertificateAndKeyPair { 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 2d12f73a9e..99e9f50778 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -72,6 +72,7 @@ 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 @@ -513,10 +514,12 @@ abstract class AbstractNode(val configuration: NodeConfiguration, // CorDapp will be generated. generatedCordapps += VirtualCordapp.generateSimpleNotaryCordapp(versionInfo) } + val blacklistedCerts = if (configuration.devMode) emptyList() else DEV_CERTIFICATES return JarScanningCordappLoader.fromDirectories( configuration.cordappDirectories, versionInfo, - extraCordapps = generatedCordapps + extraCordapps = generatedCordapps, + blacklistedCerts = blacklistedCerts ) } 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 b1ba5f9f29..9bd47c564a 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,6 +26,7 @@ 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 @@ -38,7 +39,8 @@ import kotlin.streams.toList */ class JarScanningCordappLoader private constructor(private val cordappJarPaths: List, private val versionInfo: VersionInfo = VersionInfo.UNKNOWN, - extraCordapps: List) : CordappLoaderTemplate() { + extraCordapps: List, + private val blacklistedCordappSigners: List = emptyList()) : CordappLoaderTemplate() { override val cordapps: List by lazy { loadCordapps() + extraCordapps @@ -64,10 +66,11 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: */ fun fromDirectories(cordappDirs: Collection, versionInfo: VersionInfo = VersionInfo.UNKNOWN, - extraCordapps: List = emptyList()): JarScanningCordappLoader { + extraCordapps: List = emptyList(), + blacklistedCerts: 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) + return JarScanningCordappLoader(paths, versionInfo, extraCordapps, blacklistedCerts) } /** @@ -75,9 +78,9 @@ 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()): JarScanningCordappLoader { + fun fromJarUrls(scanJars: List, versionInfo: VersionInfo = VersionInfo.UNKNOWN, extraCordapps: List = emptyList(), blacklistedCerts: List = emptyList()): JarScanningCordappLoader { val paths = scanJars.map { it.restricted() } - return JarScanningCordappLoader(paths, versionInfo, extraCordapps) + return JarScanningCordappLoader(paths, versionInfo, extraCordapps, blacklistedCerts) } private fun URL.restricted(rootPackageName: String? = null) = RestrictedURL(this, rootPackageName) @@ -106,6 +109,20 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: true } } + .filter { + if (blacklistedCordappSigners.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()) + 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 }}.") + false + } + } + } cordapps.forEach { CordappInfoResolver.register(it.cordappClasses, it.info) } return cordapps } 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 9eba3d64ef..feb4e7b476 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,6 +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 org.assertj.core.api.Assertions.assertThat import org.junit.Test import java.nio.file.Paths @@ -142,4 +143,25 @@ class JarScanningCordappLoaderTest { val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN.copy(platformVersion = 2)) assertThat(loader.cordapps).hasSize(1) } + + @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()) + 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) + 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) + assertThat(loader.cordapps).hasSize(1) + } } diff --git a/node/src/test/resources/net/corda/node/internal/cordapp/signed/signed-by-dev-key.jar b/node/src/test/resources/net/corda/node/internal/cordapp/signed/signed-by-dev-key.jar new file mode 100644 index 0000000000..beb401a992 Binary files /dev/null and b/node/src/test/resources/net/corda/node/internal/cordapp/signed/signed-by-dev-key.jar differ diff --git a/node/src/test/resources/net/corda/node/internal/cordapp/signed/signed-by-two-keys.jar b/node/src/test/resources/net/corda/node/internal/cordapp/signed/signed-by-two-keys.jar new file mode 100644 index 0000000000..c5360a0576 Binary files /dev/null and b/node/src/test/resources/net/corda/node/internal/cordapp/signed/signed-by-two-keys.jar differ