From 58e0e8c1f5e0abfe4ff7b160d6476b25d1502c28 Mon Sep 17 00:00:00 2001 From: Andrzej Cichocki Date: Fri, 16 Mar 2018 16:26:14 +0000 Subject: [PATCH] ENT-1439 Delegate SecureRandom to thread-local (#551) * Use SHA1PRNG only * Minimise CryptoUtils diff --- .../core/crypto/CordaSecurityProvider.kt | 30 ++++++++ .../corda/core/crypto/internal/ProviderMap.kt | 17 ++--- .../net/corda/core/crypto/SecureRandomTest.kt | 73 +++++++++++++++++++ 3 files changed, 111 insertions(+), 9 deletions(-) create mode 100644 core/src/test/kotlin/net/corda/core/crypto/SecureRandomTest.kt diff --git a/core/src/main/kotlin/net/corda/core/crypto/CordaSecurityProvider.kt b/core/src/main/kotlin/net/corda/core/crypto/CordaSecurityProvider.kt index ca8a077354..b918c09bcb 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CordaSecurityProvider.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CordaSecurityProvider.kt @@ -14,6 +14,11 @@ import net.corda.core.crypto.CordaObjectIdentifier.COMPOSITE_KEY import net.corda.core.crypto.CordaObjectIdentifier.COMPOSITE_SIGNATURE import org.bouncycastle.asn1.ASN1ObjectIdentifier import java.security.Provider +import io.netty.util.concurrent.FastThreadLocal +import net.corda.core.internal.VisibleForTesting +import java.security.* + +internal const val CORDA_SECURE_RANDOM_ALGORITHM = "CordaPRNG" class CordaSecurityProvider : Provider(PROVIDER_NAME, 0.1, "$PROVIDER_NAME security provider wrapper") { companion object { @@ -27,6 +32,8 @@ class CordaSecurityProvider : Provider(PROVIDER_NAME, 0.1, "$PROVIDER_NAME secur put("Alg.Alias.KeyFactory.OID.$COMPOSITE_KEY", CompositeKey.KEY_ALGORITHM) put("Alg.Alias.Signature.$COMPOSITE_SIGNATURE", CompositeSignature.SIGNATURE_ALGORITHM) put("Alg.Alias.Signature.OID.$COMPOSITE_SIGNATURE", CompositeSignature.SIGNATURE_ALGORITHM) + // Assuming this Provider is the first SecureRandom Provider, this algorithm is the SecureRandom default: + putService(DelegatingSecureRandomService(this, "SHA1PRNG")) // The SHA1PRNG impl doesn't use a global lock. } } @@ -38,3 +45,26 @@ object CordaObjectIdentifier { @JvmField val COMPOSITE_SIGNATURE = ASN1ObjectIdentifier("2.25.30086077608615255153862931087626791003") } + +internal class DelegatingSecureRandomService internal constructor( + provider: CordaSecurityProvider, algorithm: String) : Provider.Service( + provider, type, CORDA_SECURE_RANDOM_ALGORITHM, DelegatingSecureRandomSpi::class.java.name, null, null) { + internal companion object { + internal val type = "SecureRandom" + } + + internal val instance = DelegatingSecureRandomSpi(algorithm) + override fun newInstance(constructorParameter: Any?) = instance +} + +internal class DelegatingSecureRandomSpi internal constructor(algorithm: String) : SecureRandomSpi() { + private val threadLocalSecureRandom = object : FastThreadLocal() { + override fun initialValue() = SecureRandom.getInstance(algorithm) + } + + override fun engineSetSeed(seed: ByteArray) = threadLocalSecureRandom.get().setSeed(seed) + override fun engineNextBytes(bytes: ByteArray) = threadLocalSecureRandom.get().nextBytes(bytes) + override fun engineGenerateSeed(numBytes: Int): ByteArray? = threadLocalSecureRandom.get().generateSeed(numBytes) + @VisibleForTesting + internal fun currentThreadSecureRandom() = threadLocalSecureRandom.get() +} diff --git a/core/src/main/kotlin/net/corda/core/crypto/internal/ProviderMap.kt b/core/src/main/kotlin/net/corda/core/crypto/internal/ProviderMap.kt index be3beb0066..0fad2bc466 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/internal/ProviderMap.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/internal/ProviderMap.kt @@ -1,15 +1,16 @@ package net.corda.core.crypto.internal +import net.corda.core.crypto.CORDA_SECURE_RANDOM_ALGORITHM import net.corda.core.crypto.CordaSecurityProvider import net.corda.core.crypto.Crypto.EDDSA_ED25519_SHA512 import net.corda.core.crypto.Crypto.decodePrivateKey import net.corda.core.crypto.Crypto.decodePublicKey import net.corda.core.crypto.DummySecureRandom +import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.X509EdDSAEngine import net.corda.core.utilities.SgxSupport import net.i2p.crypto.eddsa.EdDSAEngine import net.i2p.crypto.eddsa.EdDSASecurityProvider -import org.apache.commons.lang.SystemUtils import org.bouncycastle.asn1.ASN1ObjectIdentifier import org.bouncycastle.asn1.pkcs.PrivateKeyInfo import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo @@ -45,12 +46,10 @@ internal val bouncyCastlePQCProvider = BouncyCastlePQCProvider().apply { // i.e. if someone removes a Provider and then he/she adds a new one with the same name. // The val is private to avoid any harmful state changes. internal val providerMap = listOf(cordaBouncyCastleProvider, cordaSecurityProvider, bouncyCastlePQCProvider).map { it.name to it }.toMap() -internal val platformSecureRandomFactory: () -> SecureRandom = when { - SgxSupport.isInsideEnclave -> { - { DummySecureRandom } - } - SystemUtils.IS_OS_LINUX -> { - { SecureRandom.getInstance("NativePRNGNonBlocking") } - } - else -> SecureRandom::getInstanceStrong +@VisibleForTesting +internal val platformSecureRandom = when { + SgxSupport.isInsideEnclave -> DummySecureRandom + else -> SecureRandom.getInstance(CORDA_SECURE_RANDOM_ALGORITHM) } + +internal fun platformSecureRandomFactory() = platformSecureRandom // To minimise diff of CryptoUtils against open-source. diff --git a/core/src/test/kotlin/net/corda/core/crypto/SecureRandomTest.kt b/core/src/test/kotlin/net/corda/core/crypto/SecureRandomTest.kt new file mode 100644 index 0000000000..8e2884e70d --- /dev/null +++ b/core/src/test/kotlin/net/corda/core/crypto/SecureRandomTest.kt @@ -0,0 +1,73 @@ +package net.corda.core.crypto + +import net.corda.core.crypto.internal.cordaSecurityProvider +import net.corda.core.internal.concurrent.fork +import net.corda.core.internal.join +import net.corda.core.utilities.getOrThrow +import org.junit.Test +import java.security.SecureRandom +import java.util.concurrent.Executors +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertNotSame +import kotlin.test.assertSame + +class SecureRandomTest { + private companion object { + private val getSpi = SecureRandom::class.java.getDeclaredMethod("getSecureRandomSpi").apply { isAccessible = true } + private fun SecureRandom.spi() = getSpi.invoke(this) + + init { + newSecureRandom() // Ensure all globals installed before running tests. + } + } + + @Test + fun `newSecureRandom returns a global that delegates to thread-local`() { + val sr = newSecureRandom() + assertSame(sr, newSecureRandom()) + checkDelegatesToThreadLocal(sr) + } + + @Test + fun `regular SecureRandom delegates to thread-local`() { + val sr = SecureRandom() + assertSame(sr.spi(), SecureRandom().spi()) + checkDelegatesToThreadLocal(sr) + } + + @Test + fun `regular SecureRandom with seed delegates to thread-local`() { + val sr = SecureRandom(byteArrayOf(1, 2, 3)) + assertSame(sr.spi(), SecureRandom(byteArrayOf(4, 5, 6)).spi()) + checkDelegatesToThreadLocal(sr) + } + + @Test + fun `SecureRandom#getInstance makes a SecureRandom that delegates to thread-local`() { + CORDA_SECURE_RANDOM_ALGORITHM.let { + val sr = SecureRandom.getInstance(it) + assertEquals(it, sr.algorithm) + assertSame(sr.spi(), SecureRandom.getInstance(it).spi()) + checkDelegatesToThreadLocal(sr) + } + } + + private fun checkDelegatesToThreadLocal(sr: SecureRandom) { + val spi = sr.spi() as DelegatingSecureRandomSpi + val fg = spi.currentThreadSecureRandom() + val e = Executors.newSingleThreadExecutor() + val bg = e.fork(spi::currentThreadSecureRandom).getOrThrow() + assertNotSame(fg, bg) // Background thread got a distinct instance. + // Each thread always gets the same instance: + assertSame(fg, spi.currentThreadSecureRandom()) + assertSame(bg, e.fork(spi::currentThreadSecureRandom).getOrThrow()) + e.join() + assertSame(fg.provider, bg.provider) + assertNotSame(cordaSecurityProvider, fg.provider) + assertEquals(fg.algorithm, bg.algorithm) + assertNotEquals(CORDA_SECURE_RANDOM_ALGORITHM, fg.algorithm) + assertSame(cordaSecurityProvider, sr.provider) + assertEquals(CORDA_SECURE_RANDOM_ALGORITHM, sr.algorithm) + } +}