mirror of
https://github.com/corda/corda.git
synced 2025-01-18 02:39:51 +00:00
CORDA-2801: Test to check compatibility between TLS 1.2 and TLS 1.3 (#4993)
The test is currently disabled till we move to Java 11 (or beyond) when TLS 1.3 becomes available as part of JDK. Local testing been performed with Open JDK 12 (12+33) and the test is passing.
This commit is contained in:
parent
09ba5c2652
commit
97d1c80e30
@ -0,0 +1,220 @@
|
|||||||
|
package net.corda.nodeapi.internal.crypto
|
||||||
|
|
||||||
|
import net.corda.core.crypto.newSecureRandom
|
||||||
|
import net.corda.core.utilities.Try
|
||||||
|
import net.corda.core.utilities.contextLogger
|
||||||
|
import net.corda.nodeapi.internal.config.CertificateStore
|
||||||
|
import net.corda.nodeapi.internal.protonwrapper.netty.init
|
||||||
|
import org.assertj.core.api.Assertions
|
||||||
|
import org.junit.Ignore
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TemporaryFolder
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.junit.runners.Parameterized
|
||||||
|
import java.io.DataInputStream
|
||||||
|
import java.io.DataOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.net.InetAddress
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import javax.net.ssl.*
|
||||||
|
import kotlin.concurrent.thread
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFalse
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
import kotlin.test.assertNotNull
|
||||||
|
import javax.net.ssl.SNIHostName
|
||||||
|
import javax.net.ssl.StandardConstants
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This test checks compatibility of TLS 1.2 and 1.3 communication using different cipher suites with SNI header
|
||||||
|
*/
|
||||||
|
@Ignore("Disabled till we switched to Java 11 where TLS 1.3 becomes available")
|
||||||
|
@RunWith(Parameterized::class)
|
||||||
|
class TlsDiffProtocolsTest(private val serverAlgo: String, private val clientAlgo: String,
|
||||||
|
private val cipherSuites: CipherSuites, private val shouldFail: Boolean,
|
||||||
|
private val serverProtocols: TlsProtocols, private val clientProtocols: TlsProtocols) {
|
||||||
|
companion object {
|
||||||
|
@Parameterized.Parameters(name = "ServerAlgo: {0}, ClientAlgo: {1}, CipherSuites: {2}, Should fail: {3}, ServerProtocols: {4}, ClientProtocols: {5}")
|
||||||
|
@JvmStatic
|
||||||
|
fun data(): List<Array<Any>> {
|
||||||
|
|
||||||
|
val allAlgos = listOf("ec", "rsa")
|
||||||
|
return allAlgos.flatMap {
|
||||||
|
serverAlgo -> allAlgos.flatMap {
|
||||||
|
clientAlgo -> listOf(
|
||||||
|
// newServerOldClient
|
||||||
|
arrayOf(serverAlgo, clientAlgo, Companion.CipherSuites.CIPHER_SUITES_ALL, false, Companion.TlsProtocols.BOTH, Companion.TlsProtocols.ONE_2),
|
||||||
|
// oldServerNewClient
|
||||||
|
arrayOf(serverAlgo, clientAlgo, Companion.CipherSuites.CIPHER_SUITES_ALL, false, Companion.TlsProtocols.ONE_2, Companion.TlsProtocols.BOTH),
|
||||||
|
// newServerNewClient
|
||||||
|
arrayOf(serverAlgo, clientAlgo, Companion.CipherSuites.CIPHER_SUITES_ALL, false, Companion.TlsProtocols.BOTH, Companion.TlsProtocols.BOTH),
|
||||||
|
// TLS 1.2 eliminated state
|
||||||
|
arrayOf(serverAlgo, clientAlgo, Companion.CipherSuites.CIPHER_SUITES_ALL, false, Companion.TlsProtocols.ONE_3, Companion.TlsProtocols.ONE_3),
|
||||||
|
// Old client connecting post TLS 1.2 eliminated state
|
||||||
|
arrayOf(serverAlgo, clientAlgo, Companion.CipherSuites.CIPHER_SUITES_ALL, true, Companion.TlsProtocols.ONE_3, Companion.TlsProtocols.ONE_2)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val logger = contextLogger()
|
||||||
|
|
||||||
|
enum class TlsProtocols(val versions: Array<String>) {
|
||||||
|
BOTH(arrayOf("TLSv1.2", "TLSv1.3")),
|
||||||
|
ONE_2(arrayOf("TLSv1.2")),
|
||||||
|
ONE_3(arrayOf("TLSv1.3"))
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class CipherSuites(val algos: Array<String>) {
|
||||||
|
CIPHER_SUITES_ALL(arrayOf(
|
||||||
|
// 1.3 only
|
||||||
|
"TLS_AES_128_GCM_SHA256",
|
||||||
|
"TLS_CHACHA20_POLY1305_SHA256",
|
||||||
|
// 1.2 only
|
||||||
|
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
|
||||||
|
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
@JvmField
|
||||||
|
val tempFolder = TemporaryFolder()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testClientServerTlsExchange() {
|
||||||
|
|
||||||
|
//System.setProperty("javax.net.debug", "all")
|
||||||
|
|
||||||
|
logger.info("Testing: ServerAlgo: $serverAlgo, ClientAlgo: $clientAlgo, Suites: $cipherSuites, Server protocols: $serverProtocols, Client protocols: $clientProtocols, Should fail: $shouldFail")
|
||||||
|
|
||||||
|
val trustStore = CertificateStore.fromResource("net/corda/nodeapi/internal/crypto/keystores/trust.jks", "trustpass", "trustpass")
|
||||||
|
val rootCa = trustStore.value.getCertificate("root")
|
||||||
|
|
||||||
|
val serverKeyStore = CertificateStore.fromResource("net/corda/nodeapi/internal/crypto/keystores/float_$serverAlgo.jks", "floatpass", "floatpass")
|
||||||
|
val serverCa = serverKeyStore.value.getCertificateAndKeyPair("floatcert", "floatpass")
|
||||||
|
|
||||||
|
val clientKeyStore = CertificateStore.fromResource("net/corda/nodeapi/internal/crypto/keystores/bridge_$clientAlgo.jks", "bridgepass", "bridgepass")
|
||||||
|
//val clientCa = clientKeyStore.value.getCertificateAndKeyPair("bridgecert", "bridgepass")
|
||||||
|
|
||||||
|
val serverSocketFactory = createSslContext(serverKeyStore, trustStore).serverSocketFactory
|
||||||
|
val clientSocketFactory = createSslContext(clientKeyStore, trustStore).socketFactory
|
||||||
|
|
||||||
|
val sniServerName = "myServerName.com"
|
||||||
|
val serverSocket = (serverSocketFactory.createServerSocket(0) as SSLServerSocket).apply {
|
||||||
|
// use 0 to get first free socket
|
||||||
|
val serverParams = SSLParameters(cipherSuites.algos, serverProtocols.versions)
|
||||||
|
serverParams.wantClientAuth = true
|
||||||
|
serverParams.needClientAuth = true
|
||||||
|
serverParams.endpointIdentificationAlgorithm = null // Reconfirm default no server name indication, use our own validator.
|
||||||
|
|
||||||
|
// SNI server setup
|
||||||
|
serverParams.sniMatchers = listOf(SNIHostName.createSNIMatcher(sniServerName))
|
||||||
|
|
||||||
|
sslParameters = serverParams
|
||||||
|
useClientMode = false
|
||||||
|
}
|
||||||
|
|
||||||
|
val clientSocket = (clientSocketFactory.createSocket() as SSLSocket).apply {
|
||||||
|
val clientParams = SSLParameters(cipherSuites.algos, clientProtocols.versions)
|
||||||
|
clientParams.endpointIdentificationAlgorithm = null // Reconfirm default no server name indication, use our own validator.
|
||||||
|
// SNI Client setup
|
||||||
|
clientParams.serverNames = listOf(SNIHostName(sniServerName))
|
||||||
|
sslParameters = clientParams
|
||||||
|
useClientMode = true
|
||||||
|
// We need to specify this explicitly because by default the client binds to 'localhost' and we want it to bind
|
||||||
|
// to whatever <hostname> resolves to(as that's what the server binds to). In particular on Debian <hostname>
|
||||||
|
// resolves to 127.0.1.1 instead of the external address of the interface, so the ssl handshake fails.
|
||||||
|
bind(InetSocketAddress(InetAddress.getLocalHost(), 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
val lock = Object()
|
||||||
|
var done = false
|
||||||
|
var serverError = false
|
||||||
|
|
||||||
|
val testPhrase = "Hello World"
|
||||||
|
val serverThread = thread {
|
||||||
|
try {
|
||||||
|
val sslServerSocket = serverSocket.accept() as SSLSocket
|
||||||
|
assertTrue(sslServerSocket.isConnected)
|
||||||
|
|
||||||
|
// Validate SNI once connected
|
||||||
|
val extendedSession = sslServerSocket.session as ExtendedSSLSession
|
||||||
|
val requestedNames = extendedSession.requestedServerNames
|
||||||
|
assertNotNull(requestedNames)
|
||||||
|
assertEquals(1, requestedNames.size)
|
||||||
|
val serverName = requestedNames[0]
|
||||||
|
assertEquals(StandardConstants.SNI_HOST_NAME, serverName.type)
|
||||||
|
val serverHostName = serverName as SNIHostName
|
||||||
|
assertEquals(sniServerName, serverHostName.asciiName)
|
||||||
|
|
||||||
|
// Validate test phrase received
|
||||||
|
val serverInput = DataInputStream(sslServerSocket.inputStream)
|
||||||
|
val receivedString = serverInput.readUTF()
|
||||||
|
assertEquals(testPhrase, receivedString)
|
||||||
|
synchronized(lock) {
|
||||||
|
done = true
|
||||||
|
lock.notifyAll()
|
||||||
|
}
|
||||||
|
sslServerSocket.close()
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
serverError = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clientSocket.connect(InetSocketAddress(InetAddress.getLocalHost(), serverSocket.localPort))
|
||||||
|
assertTrue(clientSocket.isConnected)
|
||||||
|
|
||||||
|
// Double check hostname manually
|
||||||
|
val peerChainTry = Try.on { clientSocket.session.peerCertificates.x509 }
|
||||||
|
assertEquals(!shouldFail, peerChainTry.isSuccess, "Unexpected outcome: $peerChainTry")
|
||||||
|
when(peerChainTry) {
|
||||||
|
is Try.Success -> {
|
||||||
|
val peerChain = peerChainTry.getOrThrow()
|
||||||
|
val peerX500Principal = peerChain[0].subjectX500Principal
|
||||||
|
assertEquals(serverCa.certificate.subjectX500Principal, peerX500Principal)
|
||||||
|
X509Utilities.validateCertificateChain(rootCa, peerChain)
|
||||||
|
with(DataOutputStream(clientSocket.outputStream)) {
|
||||||
|
writeUTF(testPhrase)
|
||||||
|
}
|
||||||
|
var timeout = 0
|
||||||
|
synchronized(lock) {
|
||||||
|
while (!done) {
|
||||||
|
timeout++
|
||||||
|
if (timeout > 10) throw IOException("Timed out waiting for server to complete")
|
||||||
|
lock.wait(1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clientSocket.close()
|
||||||
|
serverThread.join(1000)
|
||||||
|
assertFalse { serverError }
|
||||||
|
serverSocket.close()
|
||||||
|
assertTrue(done)
|
||||||
|
}
|
||||||
|
is Try.Failure -> {
|
||||||
|
Assertions.assertThatThrownBy {
|
||||||
|
peerChainTry.getOrThrow()
|
||||||
|
}.isInstanceOf(SSLPeerUnverifiedException::class.java)
|
||||||
|
|
||||||
|
// Tidy-up in case of failure
|
||||||
|
clientSocket.close()
|
||||||
|
serverSocket.close()
|
||||||
|
serverThread.interrupt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createSslContext(keyStore: CertificateStore, trustStore: CertificateStore): SSLContext {
|
||||||
|
return SSLContext.getInstance("TLS").apply {
|
||||||
|
val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
|
||||||
|
keyManagerFactory.init(keyStore)
|
||||||
|
val keyManagers = keyManagerFactory.keyManagers
|
||||||
|
val trustMgrFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
||||||
|
trustMgrFactory.init(trustStore)
|
||||||
|
val trustManagers = trustMgrFactory.trustManagers
|
||||||
|
init(keyManagers, trustManagers, newSecureRandom())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
These files been produced with KeyTool using commands from V3 Float/Bridge setup here:
|
||||||
|
https://docs.corda.r3.com/bridge-configuration-file.html#complete-example
|
||||||
|
|
||||||
|
More specifically the following been executed on Windows:
|
||||||
|
// Trust Root with EC algo
|
||||||
|
keytool.exe -genkeypair -keyalg EC -keysize 256 -alias floatroot -validity 1000 -dname "CN=Float Root,O=Local Only,L=London,C=GB" -ext bc:ca:true,pathlen:1 -keystore floatca.jks -storepass capass -keypass cakeypass
|
||||||
|
|
||||||
|
// Bridge and Float with EC
|
||||||
|
keytool.exe -genkeypair -keyalg EC -keysize 256 -alias bridgecert -validity 1000 -dname "CN=Bridge Local,O=Local Only,L=London,C=GB" -ext bc:ca:false -keystore bridge_ec.jks -storepass bridgepass -keypass bridgepass
|
||||||
|
keytool.exe -genkeypair -keyalg EC -keysize 256 -alias floatcert -validity 1000 -dname "CN=Float Local,O=Local Only,L=London,C=GB" -ext bc:ca:false -keystore float_ec.jks -storepass floatpass -keypass floatpass
|
||||||
|
|
||||||
|
// Bridge and Float with RSA
|
||||||
|
keytool.exe -genkeypair -keyalg RSA -keysize 1024 -alias bridgecert -validity 1000 -dname "CN=Bridge Local,O=Local Only,L=London,C=GB" -ext bc:ca:false -keystore bridge_rsa.jks -storepass bridgepass -keypass bridgepass
|
||||||
|
keytool.exe -genkeypair -keyalg RSA -keysize 1024 -alias floatcert -validity 1000 -dname "CN=Float Local,O=Local Only,L=London,C=GB" -ext bc:ca:false -keystore float_rsa.jks -storepass floatpass -keypass floatpass
|
||||||
|
|
||||||
|
// Export Trust root for subsequent chaining
|
||||||
|
keytool.exe -exportcert -rfc -alias floatroot -keystore floatca.jks -storepass capass -keypass cakeypass > root.pem
|
||||||
|
keytool.exe -importcert -noprompt -file root.pem -alias root -keystore trust.jks -storepass trustpass
|
||||||
|
|
||||||
|
// Create a chain for EC Bridge
|
||||||
|
keytool.exe -certreq -alias bridgecert -keystore bridge_ec.jks -storepass bridgepass -keypass bridgepass |keytool.exe -gencert -ext ku:c=dig,keyEncipherment -ext: eku:true=serverAuth,clientAuth -rfc -keystore floatca.jks -alias floatroot -storepass capass -keypass cakeypass > bridge_ec.pem
|
||||||
|
type root.pem bridge_ec.pem >> bridgechain_ec.pem
|
||||||
|
keytool.exe -importcert -noprompt -file bridgechain_ec.pem -alias bridgecert -keystore bridge_ec.jks -storepass bridgepass -keypass bridgepass
|
||||||
|
|
||||||
|
// Create a chain for RSA Bridge
|
||||||
|
keytool.exe -certreq -alias bridgecert -keystore bridge_rsa.jks -storepass bridgepass -keypass bridgepass |keytool.exe -gencert -ext ku:c=dig,keyEncipherment -ext: eku:true=serverAuth,clientAuth -rfc -keystore floatca.jks -alias floatroot -storepass capass -keypass cakeypass > bridge_rsa.pem
|
||||||
|
type root.pem bridge_rsa.pem >> bridgechain_rsa.pem
|
||||||
|
keytool.exe -importcert -noprompt -file bridgechain_rsa.pem -alias bridgecert -keystore bridge_rsa.jks -storepass bridgepass -keypass bridgepass
|
||||||
|
|
||||||
|
// Create a chain for EC Float
|
||||||
|
keytool.exe -certreq -alias floatcert -keystore float_ec.jks -storepass floatpass -keypass floatpass |keytool.exe -gencert -ext ku:c=dig,keyEncipherment -ext: eku::true=serverAuth,clientAuth -rfc -keystore floatca.jks -alias floatroot -storepass capass -keypass cakeypass > float_ec.pem
|
||||||
|
type root.pem float_ec.pem >> floatchain_ec.pem
|
||||||
|
keytool.exe -importcert -noprompt -file floatchain_ec.pem -alias floatcert -keystore float_ec.jks -storepass floatpass -keypass floatpass
|
||||||
|
|
||||||
|
// Create a chain for RSA Float
|
||||||
|
keytool.exe -certreq -alias floatcert -keystore float_rsa.jks -storepass floatpass -keypass floatpass |keytool.exe -gencert -ext ku:c=dig,keyEncipherment -ext: eku::true=serverAuth,clientAuth -rfc -keystore floatca.jks -alias floatroot -storepass capass -keypass cakeypass > float_rsa.pem
|
||||||
|
type root.pem float_rsa.pem >> floatchain_rsa.pem
|
||||||
|
keytool.exe -importcert -noprompt -file floatchain_rsa.pem -alias floatcert -keystore float_rsa.jks -storepass floatpass -keypass floatpass
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue
Block a user