Add a test for OpenSslEngine which decrypts traffic (#8699)

Motivation:
I've introduced netty/netty-tcnative#421 that introduced exposing OpenSSL master key & client/server
random values with the purpose of allowing someone to log them to debug the traffic via auxiliary tools like Wireshark (see also #8653)

Modification:
Augmented OpenSslEngineTest to include a test which manually decrypts the TLS ciphertext
after exposing the masterkey + client/server random. This acts as proof that the tc-native new methods work correctly!

Result:

More tests

Signed-off-by: Farid Zakaria <farid.m.zakaria@gmail.com>
This commit is contained in:
Farid Zakaria 2019-06-28 04:41:24 -07:00 committed by Norman Maurer
parent 5b58b8e6b5
commit efe40ac17d
3 changed files with 308 additions and 4 deletions

View File

@ -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 <a href="https://tools.ietf.org/html/rfc5246">rfc5246</a>}
*/
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;
}
}

View File

@ -31,19 +31,23 @@ 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 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;
@ -54,6 +58,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;
@ -1118,6 +1123,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;

View File

@ -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);
}
}