java-topology/test/jdk/javax/net/ssl/ExtendedSSLSession/ExportKeyingMaterialTests.java
russell@unturf.com 0a580b313d undefect. CWE-407 — 63 sites patched across 27 ecosystems
Authors: russell@unturf.com · brackishbert@gmail.com · foxhop.net · TimeHexOn.com

Patches, unit tests, benchmarks, whitepaper, and outreach briefs.
Public domain — no copyright claimed. Use freely.
2026-03-26 17:11:57 -04:00

573 lines
23 KiB
Java

/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
// SunJSSE does not support dynamic system properties, no way to re-use
// system properties in samevm/agentvm mode.
/*
* @test
* @bug 8341346
* @summary Add support for exporting TLS Keying Material
* @library /javax/net/ssl/templates /test/lib /test/jdk/sun/security/pkcs11
* @build SSLEngineTemplate
* @run main/othervm ExportKeyingMaterialTests
* @run main/othervm ExportKeyingMaterialTests PKCS11
*/
import java.security.Security;
import java.util.Arrays;
import javax.crypto.SecretKey;
import javax.net.ssl.*;
import javax.net.ssl.SSLEngineResult.HandshakeStatus;
import java.nio.ByteBuffer;
import java.util.Random;
import static jdk.test.lib.Asserts.*;
/**
* A SSLEngine usage example which simplifies the presentation
* by removing the I/O and multi-threading concerns.
* <P>
* The test creates two SSLEngines, simulating a client and server.
* The "transport" layer consists two byte buffers: think of them
* as directly connected pipes.
* <P>
* Note, this is a *very* simple example: real code will be much more
* involved. For example, different threading and I/O models could be
* used, transport mechanisms could close unexpectedly, and so on.
* <P>
* When this application runs, notice that several messages
* (wrap/unwrap) pass before any application data is consumed or
* produced.
*/
public class ExportKeyingMaterialTests extends SSLEngineTemplate {
private String protocol;
private String ciphersuite;
protected ExportKeyingMaterialTests(String protocol, String ciphersuite)
throws Exception {
super();
this.protocol = protocol;
this.ciphersuite = ciphersuite;
}
/*
* Configure the engines.
*/
private void configureEngines(SSLEngine clientEngine,
SSLEngine serverEngine) {
clientEngine.setUseClientMode(true);
// Get/set parameters if needed
SSLParameters paramsClient = clientEngine.getSSLParameters();
paramsClient.setProtocols(new String[] { protocol });
paramsClient.setCipherSuites(new String[] { ciphersuite });
clientEngine.setSSLParameters(paramsClient);
serverEngine.setUseClientMode(false);
serverEngine.setNeedClientAuth(true);
// Get/set parameters if needed
//
SSLParameters paramsServer = serverEngine.getSSLParameters();
paramsServer.setProtocols(new String[] {
"TLSv1.3", "TLSv1.2", "TLSv1.1", "TLSv1", "SSLv3"
});
serverEngine.setSSLParameters(paramsServer);
}
public static void main(String[] args) throws Exception {
// Turn off the disabled Algorithms so we can also test SSLv3/TLSv1/etc.
Security.setProperty("jdk.tls.disabledAlgorithms", "");
if ((args.length > 0) && (args[0].equals("PKCS11"))) {
Security.insertProviderAt(
PKCS11Test.getSunPKCS11(PKCS11Test.getNssConfig()), 1);
}
// Exercise all of the triggers which capture data
// in the various key exchange algorithms.
// Use appropriate protocol/ciphersuite combos for TLSv1.3
new ExportKeyingMaterialTests(
"TLSv1.3", "TLS_AES_128_GCM_SHA256").runTest();
new ExportKeyingMaterialTests(
"TLSv1.3", "TLS_AES_256_GCM_SHA384").runTest();
new ExportKeyingMaterialTests(
"TLSv1.3", "TLS_CHACHA20_POLY1305_SHA256").runTest();
// Try the various GCM suites for TLSv1.2
new ExportKeyingMaterialTests(
"TLSv1.2", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384").runTest();
new ExportKeyingMaterialTests(
"TLSv1.2", "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256").runTest();
new ExportKeyingMaterialTests(
"TLSv1.2", "TLS_RSA_WITH_AES_256_GCM_SHA384").runTest();
// Try one TLSv1.2/CBC suite just for grins, the triggers are the same.
new ExportKeyingMaterialTests(
"TLSv1.2", "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256").runTest();
// Use appropriate protocol/ciphersuite combos. Some of the 1.2
// suites (e.g. GCM) can't be used in earlier TLS versions.
new ExportKeyingMaterialTests(
"TLSv1.1", "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA").runTest();
new ExportKeyingMaterialTests(
"TLSv1.1", "TLS_DHE_RSA_WITH_AES_256_CBC_SHA").runTest();
new ExportKeyingMaterialTests(
"TLSv1.1", "TLS_RSA_WITH_AES_256_CBC_SHA").runTest();
new ExportKeyingMaterialTests(
"TLSv1", "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA").runTest();
new ExportKeyingMaterialTests(
"TLSv1", "TLS_DHE_RSA_WITH_AES_256_CBC_SHA").runTest();
new ExportKeyingMaterialTests(
"TLSv1", "TLS_RSA_WITH_AES_256_CBC_SHA").runTest();
try {
new ExportKeyingMaterialTests(
"SSLv3", "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA").runTest();
throw new Exception("SSLv3 export test should not have passed");
} catch (SSLException e) {
System.out.println("SSLv3 test failed as expected");
}
System.out.println("All tests PASSED");
}
//
// Private methods that used to build the common part of the test.
//
private void runTest() throws Exception {
SSLEngineResult clientResult;
SSLEngineResult serverResult;
configureEngines(clientEngine, serverEngine);
boolean dataDone = false;
while (isOpen(clientEngine) || isOpen(serverEngine)) {
log("=================");
// client wrap
log("---Client Wrap---");
clientResult = clientEngine.wrap(clientOut, cTOs);
logEngineStatus(clientEngine, clientResult);
runDelegatedTasks(clientEngine);
// server wrap
log("---Server Wrap---");
serverResult = serverEngine.wrap(serverOut, sTOc);
logEngineStatus(serverEngine, serverResult);
runDelegatedTasks(serverEngine);
cTOs.flip();
sTOc.flip();
// client unwrap
log("---Client Unwrap---");
clientResult = clientEngine.unwrap(sTOc, clientIn);
logEngineStatus(clientEngine, clientResult);
runDelegatedTasks(clientEngine);
// server unwrap
log("---Server Unwrap---");
serverResult = serverEngine.unwrap(cTOs, serverIn);
logEngineStatus(serverEngine, serverResult);
runDelegatedTasks(serverEngine);
cTOs.compact();
sTOc.compact();
// After we've transferred all application data between the client
// and server, we close the clientEngine's outbound stream.
// This generates a close_notify handshake message, which the
// server engine receives and responds by closing itself.
if (!dataDone && (clientOut.limit() == serverIn.position()) &&
(serverOut.limit() == clientIn.position())) {
runExporterTests(
(ExtendedSSLSession) clientEngine.getSession(),
(ExtendedSSLSession) serverEngine.getSession());
// A sanity check to ensure we got what was sent.
checkTransfer(serverOut, clientIn);
checkTransfer(clientOut, serverIn);
log("\tClosing clientEngine's *OUTBOUND*...");
clientEngine.closeOutbound();
logEngineStatus(clientEngine);
dataDone = true;
log("\tClosing serverEngine's *OUTBOUND*...");
serverEngine.closeOutbound();
logEngineStatus(serverEngine);
}
}
}
private static void runExporterTests(
ExtendedSSLSession clientSession,
ExtendedSSLSession serverSession) throws Exception {
SecretKey clientKey, serverKey;
// Create output arrays
byte[] clientBytes, serverBytes;
// Create various input arrays and fill with junk.
Random random = new Random();
byte[] bytes = new byte[20];
random.nextBytes(bytes);
// Slightly change 1 byte in the middle
byte[] bytesDiff = Arrays.copyOf(bytes, bytes.length);
bytesDiff[bytes.length/2]++;
byte[] bytesDiffSize = new byte[21];
random.nextBytes(bytesDiffSize);
// Run a bunch of similar derivations using both the Key/Data methods,
// exercising the various valid/invalid combinations.
// We may need to adjust if it turns out that this is run with
// non-extractable keys if .equals() doesn't work.
// Inputs exactly equal.
clientKey = clientSession.exportKeyingMaterialKey(
"Generic", "hello", bytes, 128);
serverKey = serverSession.exportKeyingMaterialKey(
"Generic", "hello", bytes, 128);
assertEquals(clientKey, serverKey,
"Key: Equal inputs but exporters are not equal");
log("Key: Equal inputs test passed");
clientBytes = clientSession.exportKeyingMaterialData("hello",
bytes, 128);
serverBytes = serverSession.exportKeyingMaterialData("hello",
bytes, 128);
assertEqualsByteArray(clientBytes, serverBytes,
"Data: Equal inputs but exporters are not equal");
log("Data: Equal inputs test passed");
// Different labels, now use exportKeyingMaterialData() for coverage
clientKey = clientSession.exportKeyingMaterialKey(
"Generic", "hello", bytes, 128);
serverKey = serverSession.exportKeyingMaterialKey(
"Generic", "goodbye", bytes, 128);
assertNotEquals(clientKey, serverKey,
"Key: Different labels but exporters same");
log("Key: Different labels test passed");
clientBytes = clientSession.exportKeyingMaterialData("hello",
bytes, 128);
serverBytes = serverSession.exportKeyingMaterialData("goodbye",
bytes, 128);
assertNotEqualsByteArray(clientBytes, serverBytes,
"Data: Different labels but exporters same");
log("Data: Different labels test passed");
// Different output sizes
clientKey = clientSession.exportKeyingMaterialKey(
"Generic", "hello", bytes, 128);
serverKey = serverSession.exportKeyingMaterialKey(
"Generic", "hello", bytes, 127);
assertNotEquals(clientKey, serverKey,
"Key: Different output sizes but exporters same");
log("Key: Different output size test passed");
clientBytes = clientSession.exportKeyingMaterialData("hello",
bytes, 128);
serverBytes = serverSession.exportKeyingMaterialData("hello",
bytes, 127);
assertEquals(clientBytes.length, 128, "client length != 128");
assertEquals(serverBytes.length, 127, "server length != 127");
assertNotEqualsByteArray(clientBytes, serverBytes,
"Data: Different output sizes but exporters same");
log("Data: Different output size test passed");
// Different context values
clientKey = clientSession.exportKeyingMaterialKey(
"Generic", "hello", bytes, 128);
serverKey = serverSession.exportKeyingMaterialKey(
"Generic", "hello", bytesDiff, 128);
assertNotEquals(clientKey, serverKey,
"Key: Different context but exporters same");
log("Key: Different context test passed");
clientBytes = clientSession.exportKeyingMaterialData("hello",
bytes, 128);
serverBytes = serverSession.exportKeyingMaterialData("hello",
bytesDiff, 128);
assertNotEqualsByteArray(clientBytes, serverBytes,
"Data: Different context but exporters same");
log("Data: Different context test passed");
// Different context sizes
clientKey = clientSession.exportKeyingMaterialKey(
"Generic", "hello", bytes, 128);
serverKey = serverSession.exportKeyingMaterialKey(
"Generic", "hello", bytesDiffSize, 128);
assertNotEquals(clientKey, serverKey,
"Key: Different context sizes but exporters same");
log("Key: Different context sizes test passed");
clientBytes = clientSession.exportKeyingMaterialData("hello",
bytes, 128);
serverBytes = serverSession.exportKeyingMaterialData("hello",
bytesDiffSize, 128);
assertNotEqualsByteArray(clientBytes, serverBytes,
"Data: Different context sizes but exporters same");
log("Data: Different context sizes test passed");
// No context, but otherwise the same
clientKey = clientSession.exportKeyingMaterialKey(
"Generic", "hello", null, 128);
serverKey = serverSession.exportKeyingMaterialKey(
"Generic", "hello", null, 128);
assertEquals(clientKey, serverKey,
"Key: No context and exporters are not the same");
log("Key: No context test passed");
clientBytes = clientSession.exportKeyingMaterialData("hello",
null, 128);
serverBytes = serverSession.exportKeyingMaterialData("hello",
null, 128);
assertEqualsByteArray(clientBytes, serverBytes,
"Data: No context and exporters are not the same");
log("Data: No context test passed");
// Smaller key size
clientKey = clientSession.exportKeyingMaterialKey(
"Generic", "hello", bytes, 5);
serverKey = serverSession.exportKeyingMaterialKey(
"Generic", "hello", bytes, 5);
assertEquals(clientKey, serverKey,
"Key: Smaller key size should be the same");
log("Key: Smaller key size test passed");
clientBytes = clientSession.exportKeyingMaterialData("hello",
bytes, 5);
serverBytes = serverSession.exportKeyingMaterialData("hello",
bytes, 5);
assertEqualsByteArray(clientBytes, serverBytes,
"Data: Smaller key size should be the same");
log("Data: Smaller key size test passed");
// Check error conditions
try {
clientSession.exportKeyingMaterialKey(null, "hello", bytes, 32);
throw new Exception("null keyAlg accepted");
} catch (NullPointerException e) {
log("null keyAlg test passed");
}
try {
clientSession.exportKeyingMaterialKey("", "hello", bytes, 32);
throw new Exception("empty keyAlg accepted");
} catch (IllegalArgumentException e) {
log("empty keyAlg test passed");
}
try {
clientSession.exportKeyingMaterialData("hello", bytes, -1);
throw new Exception("negative length accepted");
} catch (IllegalArgumentException e) {
log("negative length test passed");
}
try {
clientSession.exportKeyingMaterialData("hello", bytes, 0);
throw new Exception("zero length accepted");
} catch (IllegalArgumentException e) {
log("zero length test passed");
}
try {
clientSession.exportKeyingMaterialData(null, bytes, 128);
throw new Exception("null label accepted");
} catch (NullPointerException e) {
log("null label test passed");
}
try {
clientSession.exportKeyingMaterialData("", bytes, 128);
throw new Exception("empty label accepted");
} catch (IllegalArgumentException e) {
log("empty label test passed");
}
switch (clientSession.getProtocol()) {
case "TLSv1.3":
// 249 bytes is the max label we can accept (<7..255>, since
// "tls13 " is added in HkdfLabel)
String longString =
"12345678901234567890123456789012345678901234567890" +
"12345678901234567890123456789012345678901234567890" +
"12345678901234567890123456789012345678901234567890" +
"12345678901234567890123456789012345678901234567890" +
"1234567890123456789012345678901234567890123456789";
clientSession.exportKeyingMaterialData(longString, bytes, 128);
log("large label test passed in TLSv1.3");
try {
clientSession.exportKeyingMaterialData(
longString + "0", bytes, 128);
throw new Exception("too large label accepted in TLSv1.3");
} catch (IllegalArgumentException e) {
log("too large label test passed in TLSv1.3");
}
// 255 bytes is the max context we can accept (<0..255>)
clientSession.exportKeyingMaterialData(
longString, new byte[255], 128);
log("large context test passed in TLSv1.3");
try {
clientSession.exportKeyingMaterialData(
longString, new byte[256], 128);
throw new Exception("too large context accepted in TLSv1.3");
} catch (IllegalArgumentException e) {
log("too large context test passed in TLSv1.3");
}
// RFC 5869 says 255*HashLen bytes is the max length we can accept.
// So we'll choose something a bit bigger than the largest
// hashLen/ciphertext which is 384 (48 bytes) so this will always
// fail.
try {
clientSession.exportKeyingMaterialData(
longString, new byte[256], 12240);
throw new Exception("too large length accepted in TLSv1.3");
} catch (IllegalArgumentException e) {
log("too large length test passed in TLSv1.3");
}
// Do a null/byte[0] comparison. Exporters should be the same.
clientBytes = clientSession.exportKeyingMaterialData("hello",
null, 128);
serverBytes = serverSession.exportKeyingMaterialData("hello",
new byte[0], 128);
assertEqualsByteArray(clientBytes, serverBytes,
"Data: null vs. empty context should be the same");
break;
case "TLSv1":
case "TLSv1.1":
case "TLSv1.2":
// Don't see a limit of the label.length or output length.
// Check for large context.length
try {
clientSession.exportKeyingMaterialData("hello",
new byte[1 << 16], 128);
throw new Exception("large context accepted in " +
"TLSv1/TLSv1.1/TLSv1.2");
} catch (IllegalArgumentException e) {
log("large context passed in TLSv1/TLSv1.1/TLSv1.2");
}
// Do a null/byte[0] comparison. Should NOT be the same.
clientBytes = clientSession.exportKeyingMaterialData(
"hello", null, 128);
serverBytes = serverSession.exportKeyingMaterialData(
"hello", new byte[0], 128);
assertNotEqualsByteArray(clientBytes, serverBytes,
"empty vs. null context but exporters same");
log("Data: empty vs. null context, " +
"different exporters test passed");
break;
default:
throw new RuntimeException("Unknown protocol: " + clientSession.getProtocol());
}
}
static boolean isOpen(SSLEngine engine) {
return (!engine.isOutboundDone() || !engine.isInboundDone());
}
private static void logEngineStatus(SSLEngine engine) {
log("\tCurrent HS State: " + engine.getHandshakeStatus());
log("\tisInboundDone() : " + engine.isInboundDone());
log("\tisOutboundDone(): " + engine.isOutboundDone());
}
private static void logEngineStatus(
SSLEngine engine, SSLEngineResult result) {
log("\tResult Status : " + result.getStatus());
log("\tResult HS Status : " + result.getHandshakeStatus());
log("\tEngine HS Status : " + engine.getHandshakeStatus());
log("\tisInboundDone() : " + engine.isInboundDone());
log("\tisOutboundDone() : " + engine.isOutboundDone());
log("\tMore Result : " + result);
}
private static void log(String message) {
System.err.println(message);
}
// If the result indicates that we have outstanding tasks to do,
// go ahead and run them in this thread.
protected static void runDelegatedTasks(SSLEngine engine) throws Exception {
if (engine.getHandshakeStatus() == HandshakeStatus.NEED_TASK) {
Runnable runnable;
while ((runnable = engine.getDelegatedTask()) != null) {
log(" running delegated task...");
runnable.run();
}
HandshakeStatus hsStatus = engine.getHandshakeStatus();
if (hsStatus == HandshakeStatus.NEED_TASK) {
throw new Exception(
"handshake shouldn't need additional tasks");
}
logEngineStatus(engine);
}
}
// Simple check to make sure everything came across as expected.
static void checkTransfer(ByteBuffer a, ByteBuffer b)
throws Exception {
a.flip();
b.flip();
if (!a.equals(b)) {
throw new Exception("Data didn't transfer cleanly");
} else {
log("\tData transferred cleanly");
}
a.position(a.limit());
b.position(b.limit());
a.limit(a.capacity());
b.limit(b.capacity());
}
}