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:
parent
f9fad84ae6
commit
cc1528bdad
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user