diff --git a/handler/src/main/java/io/netty/handler/ssl/PseudoRandomFunction.java b/handler/src/main/java/io/netty/handler/ssl/PseudoRandomFunction.java new file mode 100644 index 0000000000..77eea0b4be --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/PseudoRandomFunction.java @@ -0,0 +1,94 @@ +/* + * Copyright 2019 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.ssl; + +import io.netty.util.internal.EmptyArrays; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.security.GeneralSecurityException; +import java.util.Arrays; + +/** + * This pseudorandom function (PRF) takes as input a secret, a seed, and + * an identifying label and produces an output of arbitrary length. + * + * This is used by the TLS RFC to construct/deconstruct an array of bytes into + * composite secrets. + * + * {@link rfc5246} + */ +final class PseudoRandomFunction { + + /** + * Constructor never to be called. + */ + private PseudoRandomFunction() { + } + + /** + * Use a single hash function to expand a secret and seed into an + * arbitrary quantity of output. + * + * P_hash(secret, seed) = HMAC_hash(secret, A(1) + seed) + + * HMAC_hash(secret, A(2) + seed) + + * HMAC_hash(secret, A(3) + seed) + ... + * where + indicates concatenation. + * A() is defined as: + * A(0) = seed + * A(i) = HMAC_hash(secret, A(i-1)) + * @param secret The starting secret to use for expansion + * @param label An ascii string without a length byte or trailing null character. + * @param seed The seed of the hash + * @param length The number of bytes to return + * @param algo the hmac algorithm to use + * @return The expanded secrets + * @throws IllegalArgumentException if the algo could not be found. + */ + static byte[] hash(byte[] secret, byte[] label, byte[] seed, int length, String algo) { + if (length < 0) { + throw new IllegalArgumentException("You must provide a length greater than zero."); + } + try { + Mac hmac = Mac.getInstance(algo); + hmac.init(new SecretKeySpec(secret, algo)); + /* + * P_hash(secret, seed) = HMAC_hash(secret, A(1) + seed) + + * HMAC_hash(secret, A(2) + seed) + HMAC_hash(secret, A(3) + seed) + ... + * where + indicates concatenation. A() is defined as: A(0) = seed, A(i) + * = HMAC_hash(secret, A(i-1)) + */ + + int iterations = (int) Math.ceil(length / (double) hmac.getMacLength()); + byte[] expansion = EmptyArrays.EMPTY_BYTES; + byte[] data = concat(label, seed); + byte[] A = data; + for (int i = 0; i < iterations; i++) { + A = hmac.doFinal(A); + expansion = concat(expansion, hmac.doFinal(concat(A, data))); + } + return Arrays.copyOf(expansion, length); + } catch (GeneralSecurityException e) { + throw new IllegalArgumentException("Could not find algo: " + algo, e); + } + } + + private static byte[] concat(byte[] first, byte[] second) { + byte[] result = Arrays.copyOf(first, first.length + second.length); + System.arraycopy(second, 0, result, first.length, second.length); + return result; + } +} diff --git a/handler/src/test/java/io/netty/handler/ssl/OpenSslEngineTest.java b/handler/src/test/java/io/netty/handler/ssl/OpenSslEngineTest.java index 1c69923365..2f3a373824 100644 --- a/handler/src/test/java/io/netty/handler/ssl/OpenSslEngineTest.java +++ b/handler/src/test/java/io/netty/handler/ssl/OpenSslEngineTest.java @@ -31,20 +31,24 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLEngineResult; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLParameters; import java.nio.ByteBuffer; import java.security.AlgorithmConstraints; import java.security.AlgorithmParameters; import java.security.CryptoPrimitive; import java.security.Key; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Set; import java.util.concurrent.ThreadLocalRandom; -import javax.net.ssl.SSLEngine; -import javax.net.ssl.SSLEngineResult; -import javax.net.ssl.SSLException; -import javax.net.ssl.SSLParameters; import static io.netty.handler.ssl.OpenSslTestUtils.checkShouldUseKeyManagerFactory; import static io.netty.handler.ssl.ReferenceCountedOpenSslEngine.MAX_PLAINTEXT_LENGTH; @@ -55,6 +59,7 @@ import static io.netty.handler.ssl.SslUtils.PROTOCOL_TLS_V1_1; import static io.netty.handler.ssl.SslUtils.PROTOCOL_TLS_V1_2; import static io.netty.internal.tcnative.SSL.SSL_CVERIFY_IGNORED; import static java.lang.Integer.MAX_VALUE; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; @@ -1117,6 +1122,158 @@ public class OpenSslEngineTest extends SSLEngineTest { } } + @Test + public void testExtractMasterkeyWorksCorrectly() throws Exception { + SelfSignedCertificate cert = new SelfSignedCertificate(); + serverSslCtx = SslContextBuilder.forServer(cert.key(), cert.cert()) + .sslProvider(SslProvider.OPENSSL).build(); + final SSLEngine serverEngine = + serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT); + clientSslCtx = SslContextBuilder.forClient() + .trustManager(cert.certificate()) + .sslProvider(SslProvider.OPENSSL).build(); + final SSLEngine clientEngine = + clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT); + + try { + //lets set the cipher suite to a specific one with DHE + assumeTrue("The diffie hellman cipher is not supported on your runtime.", + Arrays.asList(clientEngine.getSupportedCipherSuites()) + .contains("TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256")); + + //https://www.ietf.org/rfc/rfc5289.txt + //For cipher suites ending with _SHA256, the PRF is the TLS PRF + //[RFC5246] with SHA-256 as the hash function. The MAC is HMAC + //[RFC2104] with SHA-256 as the hash function. + clientEngine.setEnabledCipherSuites(new String[] { "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256" }); + serverEngine.setEnabledCipherSuites(new String[] { "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256" }); + + int appBufferMax = clientEngine.getSession().getApplicationBufferSize(); + int netBufferMax = clientEngine.getSession().getPacketBufferSize(); + + /* + * We'll make the input buffers a bit bigger than the max needed + * size, so that unwrap()s following a successful data transfer + * won't generate BUFFER_OVERFLOWS. + */ + ByteBuffer clientIn = ByteBuffer.allocate(appBufferMax + 50); + ByteBuffer serverIn = ByteBuffer.allocate(appBufferMax + 50); + + ByteBuffer cTOs = ByteBuffer.allocate(netBufferMax); + ByteBuffer sTOc = ByteBuffer.allocate(netBufferMax); + + ByteBuffer clientOut = ByteBuffer.wrap("Hi Server, I'm Client".getBytes()); + ByteBuffer serverOut = ByteBuffer.wrap("Hello Client, I'm Server".getBytes()); + + // This implementation is largely imitated from + // https://docs.oracle.com/javase/8/docs/technotes/ + // guides/security/jsse/samples/sslengine/SSLEngineSimpleDemo.java + // It has been simplified however without the need for running delegation tasks + + // Do handshake for SSL + // A typical handshake will usually contain the following steps: + // 1. wrap: ClientHello + // 2. unwrap: ServerHello/Cert/ServerHelloDone + // 3. wrap: ClientKeyExchange + // 4. wrap: ChangeCipherSpec + // 5. wrap: Finished + // 6. unwrap: ChangeCipherSpec + // 7. unwrap: Finished + + //set a for loop; instead of a while loop to guarantee we quit out eventually + boolean asserted = false; + for (int i = 0; i < 1000; i++) { + + clientEngine.wrap(clientOut, cTOs); + serverEngine.wrap(serverOut, sTOc); + + cTOs.flip(); + sTOc.flip(); + + clientEngine.unwrap(sTOc, clientIn); + serverEngine.unwrap(cTOs, serverIn); + + // check when the application data has fully been consumed and sent + // for both the client and server + if ((clientOut.limit() == serverIn.position()) && + (serverOut.limit() == clientIn.position())) { + byte[] serverRandom = SSL.getServerRandom(((OpenSslEngine) serverEngine).sslPointer()); + byte[] clientRandom = SSL.getClientRandom(((OpenSslEngine) clientEngine).sslPointer()); + byte[] serverMasterKey = SSL.getMasterKey(((OpenSslEngine) serverEngine).sslPointer()); + byte[] clientMasterKey = SSL.getMasterKey(((OpenSslEngine) clientEngine).sslPointer()); + + asserted = true; + assertArrayEquals(serverMasterKey, clientMasterKey); + + // let us re-read the encrypted data and decrypt it ourselves! + cTOs.flip(); + sTOc.flip(); + + // See http://tools.ietf.org/html/rfc5246#section-6.3: + // key_block = PRF(SecurityParameters.master_secret, "key expansion", + // SecurityParameters.server_random + SecurityParameters.client_random); + // + // partitioned: + // client_write_MAC_secret[SecurityParameters.hash_size] + // server_write_MAC_secret[SecurityParameters.hash_size] + // client_write_key[SecurityParameters.key_material_length] + // server_write_key[SecurityParameters.key_material_length] + + int keySize = 16; // AES is 16 bytes or 128 bits + int macSize = 32; // SHA256 is 32 bytes or 256 bits + int keyBlockSize = (2 * keySize) + (2 * macSize); + + byte[] seed = new byte[serverRandom.length + clientRandom.length]; + System.arraycopy(serverRandom, 0, seed, 0, serverRandom.length); + System.arraycopy(clientRandom, 0, seed, serverRandom.length, clientRandom.length); + byte[] keyBlock = PseudoRandomFunction.hash(serverMasterKey, + "key expansion".getBytes(CharsetUtil.US_ASCII), seed, keyBlockSize, "HmacSha256"); + + int offset = 0; + byte[] clientWriteMac = Arrays.copyOfRange(keyBlock, offset, offset + macSize); + offset += macSize; + + byte[] serverWriteMac = Arrays.copyOfRange(keyBlock, offset, offset + macSize); + offset += macSize; + + byte[] clientWriteKey = Arrays.copyOfRange(keyBlock, offset, offset + keySize); + offset += keySize; + + byte[] serverWriteKey = Arrays.copyOfRange(keyBlock, offset, offset + keySize); + offset += keySize; + + //advance the cipher text by 5 + //to take into account the TLS Record Header + cTOs.position(cTOs.position() + 5); + + byte[] ciphertext = new byte[cTOs.remaining()]; + cTOs.get(ciphertext); + + //the initialization vector is the first 16 bytes (128 bits) of the payload + byte[] clientWriteIV = Arrays.copyOfRange(ciphertext, 0, 16); + ciphertext = Arrays.copyOfRange(ciphertext, 16, ciphertext.length); + + SecretKeySpec secretKey = new SecretKeySpec(clientWriteKey, "AES"); + final IvParameterSpec ivForCBC = new IvParameterSpec(clientWriteIV); + Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); + cipher.init(Cipher.DECRYPT_MODE, secretKey, ivForCBC); + byte[] plaintext = cipher.doFinal(ciphertext); + assertTrue(new String(plaintext).startsWith("Hi Server, I'm Client")); + break; + } else { + cTOs.compact(); + sTOc.compact(); + } + } + + assertTrue("The assertions were never executed.", asserted); + } finally { + cleanupClientSslEngine(clientEngine); + cleanupServerSslEngine(serverEngine); + cert.delete(); + } + } + @Override protected SslProvider sslClientProvider() { return SslProvider.OPENSSL; diff --git a/handler/src/test/java/io/netty/handler/ssl/PseudoRandomFunctionTest.java b/handler/src/test/java/io/netty/handler/ssl/PseudoRandomFunctionTest.java new file mode 100644 index 0000000000..30fc3732a9 --- /dev/null +++ b/handler/src/test/java/io/netty/handler/ssl/PseudoRandomFunctionTest.java @@ -0,0 +1,53 @@ +/* + * Copyright 2019 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.netty.handler.ssl; + +import io.netty.util.CharsetUtil; +import org.bouncycastle.util.encoders.Hex; +import org.junit.Test; + +import static org.junit.Assert.assertArrayEquals; + +/** + * The test vectors here were provided via: + * https://www.ietf.org/mail-archive/web/tls/current/msg03416.html + */ +public class PseudoRandomFunctionTest { + + @Test + public void testPrfSha256() { + byte[] secret = Hex.decode("9b be 43 6b a9 40 f0 17 b1 76 52 84 9a 71 db 35"); + byte[] seed = Hex.decode("a0 ba 9f 93 6c da 31 18 27 a6 f7 96 ff d5 19 8c"); + byte[] label = "test label".getBytes(CharsetUtil.US_ASCII); + byte[] expected = Hex.decode( + "e3 f2 29 ba 72 7b e1 7b" + + "8d 12 26 20 55 7c d4 53" + + "c2 aa b2 1d 07 c3 d4 95" + + "32 9b 52 d4 e6 1e db 5a" + + "6b 30 17 91 e9 0d 35 c9" + + "c9 a4 6b 4e 14 ba f9 af" + + "0f a0 22 f7 07 7d ef 17" + + "ab fd 37 97 c0 56 4b ab" + + "4f bc 91 66 6e 9d ef 9b" + + "97 fc e3 4f 79 67 89 ba" + + "a4 80 82 d1 22 ee 42 c5" + + "a7 2e 5a 51 10 ff f7 01" + + "87 34 7b 66"); + byte[] actual = PseudoRandomFunction.hash(secret, label, seed, expected.length, "HmacSha256"); + assertArrayEquals(expected, actual); + } +}