From 97d1c80e30f5f068662a7452df2a29a189e29225 Mon Sep 17 00:00:00 2001 From: Viktor Kolomeyko Date: Mon, 15 Apr 2019 09:51:44 +0100 Subject: [PATCH] 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. --- .../internal/crypto/TlsDiffProtocolsTest.kt | 220 ++++++++++++++++++ .../internal/crypto/keystores/README.txt | 38 +++ .../internal/crypto/keystores/bridge_ec.jks | Bin 0 -> 1163 bytes .../internal/crypto/keystores/bridge_rsa.jks | Bin 0 -> 1803 bytes .../internal/crypto/keystores/float_ec.jks | Bin 0 -> 1161 bytes .../internal/crypto/keystores/float_rsa.jks | Bin 0 -> 1802 bytes .../internal/crypto/keystores/trust.jks | Bin 0 -> 502 bytes 7 files changed, 258 insertions(+) create mode 100644 node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/TlsDiffProtocolsTest.kt create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/crypto/keystores/README.txt create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/crypto/keystores/bridge_ec.jks create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/crypto/keystores/bridge_rsa.jks create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/crypto/keystores/float_ec.jks create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/crypto/keystores/float_rsa.jks create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/crypto/keystores/trust.jks diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/TlsDiffProtocolsTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/TlsDiffProtocolsTest.kt new file mode 100644 index 0000000000..9ed5d98e54 --- /dev/null +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/TlsDiffProtocolsTest.kt @@ -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> { + + 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) { + BOTH(arrayOf("TLSv1.2", "TLSv1.3")), + ONE_2(arrayOf("TLSv1.2")), + ONE_3(arrayOf("TLSv1.3")) + } + + enum class CipherSuites(val algos: Array) { + 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 resolves to(as that's what the server binds to). In particular on Debian + // 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()) + } + } +} \ No newline at end of file diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/crypto/keystores/README.txt b/node-api/src/test/resources/net/corda/nodeapi/internal/crypto/keystores/README.txt new file mode 100644 index 0000000000..d92321025a --- /dev/null +++ b/node-api/src/test/resources/net/corda/nodeapi/internal/crypto/keystores/README.txt @@ -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 \ No newline at end of file diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/crypto/keystores/bridge_ec.jks b/node-api/src/test/resources/net/corda/nodeapi/internal/crypto/keystores/bridge_ec.jks new file mode 100644 index 0000000000000000000000000000000000000000..2e74e4d29301ae96e90d0149d50817f7667c816a GIT binary patch literal 1163 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 Jkf4>`CjiI-n@#`# literal 0 HcmV?d00001 diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/crypto/keystores/float_ec.jks b/node-api/src/test/resources/net/corda/nodeapi/internal/crypto/keystores/float_ec.jks new file mode 100644 index 0000000000000000000000000000000000000000..c08d825b2f397a3784f85f49c4cbe7e178f4006c GIT binary patch literal 1161 zcmezO_TO6u1_mY|W&~r-w4D6JlH}B)5}-)tqlLSQfWq|#wFZ1_T-t1mER0%Af{cu; z3@q8|+)n3zHT3EHou;~D>C5s`wOuk zxgG}bDEGGB%1Ild&wyRX8lh)uUz+7_!BPiFvlN_5w41_?|GYfM= zYyvyNAksjXjUDU>CPt`lm>Jobofudag#EhxLEzK8B{lQ1)ru#5O^w`s#{ag$n_p5P zGd`|0O+IVK6t2+5RLQLO@npNg{X2|{*57h{uV1v`y{WR=rN7aO3k~uO z85#exfWmkIL^&Ue7>kHdU(JUihRyme$)Xa=N|^on|DIt+jSyxJ1_M_nMTQ*=P8%n$ zynQ0+@=$zCS-0Hs1j0c$(GAS^rcOLO84)DCB%oN(U{O|M+9`OND znV!jU?G;azmMUIkfG2~Uz$CC0h$ErNK;qOw5mZ}glnhJ^EP=_u$k5Qp$O4!QKwK)? zggX^n`ky~{?bEK^dcVJ>G2YtJBDnX(l`oSgYut~nReUTUQt5g%DBn?(Q^zk*Dn0&~ z*s~LJ_U`&~#^B+#Vw7(l>S6lP&HUmwfP=!Ogg=OS1A8=N~0qmoD!Yz*wHVJT~YeQ>oLf^mAvvE|C1ZXR)2g m>~Cjo+`I2yRdwj**VguB&7R^%9sedsh@?2Wc{zu66#xJit7yUi literal 0 HcmV?d00001 diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/crypto/keystores/float_rsa.jks b/node-api/src/test/resources/net/corda/nodeapi/internal/crypto/keystores/float_rsa.jks new file mode 100644 index 0000000000000000000000000000000000000000..c00779ae7e8d7c425fb60fa6b8b1120b4afad1eb GIT binary patch literal 1802 zcmezO_TO6u1_mY|W&~r-w4D6JlH}B)5}-)tqeaGMK-paeO-$Pi_}I9#*%(jeNe?lUhGcoYuQD>-=Qbo9Vo}+fVyX zcC(zNHgo30DrKHlzW&Q;u_4bsJ>JDJb%h+uWuK*9Npn3Vmxms0sovB%r~b_$Lq4u( zm5QaVsc*_(J>^#KT|29A!nSWkJ0C@#Zp-2qW7G_v@lo73|I>e|G}Rj}{^x~RETXHQ zoeGeASiJpi-6FmB(^#F>-7r7b>^=R-$Ea7z9~5&wNeJ`k*VLx*?#_QPODIPucVaJ6&alve5&gVM0)^&b?3+@DejNYa#5OJc!kZI3y&b#Zl zADrZUnRNbkFz1nosy5Ejiy4o5B6L<1JQk*R4n1 zz0;q^6IghP`-^ts^ln$J9kv#)_Oxn*{dvEJ)|NevdcWuu4GiydSvK_thetjwb>zOxtbzQHw&Qh7xsUzSN zxNG75xBQA{Kku%Zb+)Sb$)bmTAI>rz)|~ox`5Ms$3#u0AWIVdK37oiDBlJuSEP)Bx z37C-W44N1pFJNY3WMX28k>xyZz{AF&)#h=|mW7##mBGNnklTQhjX9KsO_<5u$&lZG z7sO!~X7kC`Q#@j<|z2*xPHoyiIGMca2?Mo{KtZtP_+ zXzXNaY-HHNw_fCXi>~>+RNakR-G5z~ZlT%svG>Sbk7(@^wl~&Y?vcxwx1i10{8{I| zdFiDRf{`Xq6L)|3EB$BwzXhk~S8=RfTH|xr=)%GW69c#YG+z*LH0#W)e~OcuwZAEe zyxD*A&W&dZDpBSiHcv5H_E%E8bdJfRiRTzY-kC~&)Y<(sA?Gm@Gb01z;zEOb19_m! zWtCYZ48$5lHfu?UG~D;flDYlHeiqM8v-)>;9~;Pm6sWR@8HliP0E-${R(57aIEx8b z00E`>85#exfTC&vL^&Ue7>me(`@d42@m0QjH*1rUjz(orp4O&L)OclfXE1PKQYaNz zU2AiSUv#p5HOqOcXG>C8jU?wBUJ@uLbGNBt^{anO3StiH^0j7lN{3u6d^SyC^0_Eo zpInXjDweZs9ED9QcFkjeB__t5z=X6Fh$EqiN#fK(5mZZQl$cBmEP;v1$k5Qp$O4#{ zKwK)?6loyL#tzO9Ou)bgGng6KnVlF|F8$A+yY^|wrFlQr%~ z*D5|15UF&%8kFxS%BkZQD3u<6Ozhc-IeT~gIb-nf+VUB7$Ij00TdZ%O3k)D&Bnq>z z8ZZMR5-AF?CJ29tHVFN*s z0GBYAPkwS@j)H$)P9;o`S(wW$CqJ=7AqZ%@ft)z6p{0R|fw6(5sfmGQlsK=Ep`nqH z1(Zugn<5Q_+1SB;U}A)NhMAF_*@=PW(*OLqYoB)Q*8BZ6jq%o&7QwwQu6&t1S>t|m zt>R+=kxJLALHUlNoH~AiQt9!>#Gaj)vv=2@GX@W@EuT?$?Cku$#rg)izyM+nl@(@T zHDG3B{BIx&;_