8341346: Add support for exporting TLS Keying Material

Reviewed-by: hchao, jnimeh, weijun
This commit is contained in:
Bradford Wetmore 2025-05-30 23:06:36 +00:00
parent 0df8c9684b
commit 2926435d22
9 changed files with 1043 additions and 20 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2005, 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2005, 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
@ -147,7 +147,7 @@ abstract class TlsPrfGenerator extends KeyGeneratorSpi {
throw new InvalidParameterException(MSG);
}
SecretKey engineGenerateKey0(boolean tls12) {
protected SecretKey engineGenerateKey0(boolean tls12) {
if (spec == null) {
throw new IllegalStateException(
"TlsPrfGenerator must be initialized");
@ -163,7 +163,7 @@ abstract class TlsPrfGenerator extends KeyGeneratorSpi {
spec.getPRFBlockSize()) :
doTLS10PRF(secret, labelBytes, spec.getSeed(), n));
try {
return new SecretKeySpec(prfBytes, "TlsPrf");
return new SecretKeySpec(prfBytes, spec.getKeyAlg());
} finally {
Arrays.fill(prfBytes, (byte)0);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2010, 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2010, 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
@ -26,6 +26,7 @@
package javax.net.ssl;
import java.util.List;
import javax.crypto.SecretKey;
/**
* Extends the {@code SSLSession} interface to support additional
@ -163,4 +164,113 @@ public abstract class ExtendedSSLSession implements SSLSession {
public List<byte[]> getStatusResponses() {
throw new UnsupportedOperationException();
}
/**
* Generates Exported Keying Material (EKM) calculated according to the
* algorithms defined in RFCs 5705/8446.
* <P>
* RFC 5705 (for (D)TLSv1.2 and earlier) calculates different EKM
* values depending on whether {@code context} is null or non-null/empty.
* RFC 8446 (TLSv1.3) treats a null context as non-null/empty.
* <P>
* {@code label} will be converted to bytes using
* the {@link java.nio.charset.StandardCharsets#UTF_8}
* character encoding.
*
* @spec https://www.rfc-editor.org/info/rfc5705
* RFC 5705: Keying Material Exporters for Transport Layer
* Security (TLS)
* @spec https://www.rfc-editor.org/info/rfc8446
* RFC 8446: The Transport Layer Security (TLS) Protocol Version 1.3
*
* @implSpec The default implementation throws
* {@code UnsupportedOperationException}.
*
* @param keyAlg the algorithm of the resultant {@code SecretKey} object.
* See the SecretKey Algorithms section in the
* <a href="{@docRoot}/../specs/security/standard-names.html#secretkey-algorithms">
* Java Security Standard Algorithm Names Specification</a>
* for information about standard secret key algorithm
* names.
* @param label the label bytes used in the EKM calculation.
* {@code label} will be converted to a {@code byte[]}
* before the operation begins.
* @param context the context bytes used in the EKM calculation, or null
* @param length the number of bytes of EKM material needed
*
* @throws SSLKeyException if the key cannot be generated
* @throws IllegalArgumentException if {@code keyAlg} is empty,
* {@code length} is non-positive, or if the {@code label} or
* {@code context} length can not be accommodated
* @throws NullPointerException if {@code keyAlg} or {@code label} is null
* @throws IllegalStateException if this session does not have the
* necessary key generation material (for example, a session
* under construction during handshaking)
* @throws UnsupportedOperationException if the underlying provider
* does not implement the operation
*
* @return a {@code SecretKey} that contains {@code length} bytes of the
* EKM material
*
* @since 25
*/
public SecretKey exportKeyingMaterialKey(String keyAlg,
String label, byte[] context, int length) throws SSLKeyException {
throw new UnsupportedOperationException(
"Underlying provider does not implement the method");
}
/**
* Generates Exported Keying Material (EKM) calculated according to the
* algorithms defined in RFCs 5705/8446.
* <P>
* RFC 5705 (for (D)TLSv1.2 and earlier) calculates different EKM
* values depending on whether {@code context} is null or non-null/empty.
* RFC 8446 (TLSv1.3) treats a null context as non-null/empty.
* <P>
* {@code label} will be converted to bytes using
* the {@link java.nio.charset.StandardCharsets#UTF_8}
* character encoding.
* <P>
* Depending on the chosen underlying key derivation mechanism, the
* raw bytes might not be extractable/exportable. In such cases, the
* {@link #exportKeyingMaterialKey(String, String, byte[], int)} method
* should be used instead to access the generated key material.
*
* @spec https://www.rfc-editor.org/info/rfc5705
* RFC 5705: Keying Material Exporters for Transport Layer
* Security (TLS)
* @spec https://www.rfc-editor.org/info/rfc8446
* RFC 8446: The Transport Layer Security (TLS) Protocol Version 1.3
*
* @implSpec The default implementation throws
* {@code UnsupportedOperationException}.
*
* @param label the label bytes used in the EKM calculation.
* {@code label} will be converted to a {@code byte[]}
* before the operation begins.
* @param context the context bytes used in the EKM calculation, or null
* @param length the number of bytes of EKM material needed
*
* @throws SSLKeyException if the key cannot be generated
* @throws IllegalArgumentException if {@code length} is non-positive,
* or if the {@code label} or {@code context} length can
* not be accommodated
* @throws NullPointerException if {@code label} is null
* @throws IllegalStateException if this session does not have the
* necessary key generation material (for example, a session
* under construction during handshaking)
* @throws UnsupportedOperationException if the underlying provider
* does not implement the operation, or if the derived
* keying material is not extractable
*
* @return a byte array of size {@code length} that contains the EKM
* material
* @since 25
*/
public byte[] exportKeyingMaterialData(
String label, byte[] context, int length) throws SSLKeyException {
throw new UnsupportedOperationException(
"Underlying provider does not implement the method");
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2005, 2022, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2005, 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
@ -45,6 +45,7 @@ import javax.crypto.SecretKey;
public class TlsPrfParameterSpec implements AlgorithmParameterSpec {
private final SecretKey secret;
private final String keyAlg;
private final String label;
private final byte[] seed;
private final int outputLength;
@ -72,13 +73,45 @@ public class TlsPrfParameterSpec implements AlgorithmParameterSpec {
public TlsPrfParameterSpec(SecretKey secret, String label,
byte[] seed, int outputLength,
String prfHashAlg, int prfHashLength, int prfBlockSize) {
if ((label == null) || (seed == null)) {
throw new NullPointerException("label and seed must not be null");
this(secret, "TlsPrf", label, seed, outputLength, prfHashAlg,
prfHashLength, prfBlockSize);
}
/**
* Constructs a new TlsPrfParameterSpec.
*
* @param secret the secret to use in the calculation (or null)
* @param keyAlg the algorithm name for the generated {@code SecretKey}
* @param label the label to use in the calculation
* @param seed the random seed to use in the calculation
* @param outputLength the length in bytes of the output key to be produced
* @param prfHashAlg the name of the TLS PRF hash algorithm to use.
* Used only for TLS 1.2+. TLS1.1 and earlier use a fixed PRF.
* @param prfHashLength the output length of the TLS PRF hash algorithm.
* Used only for TLS 1.2+.
* @param prfBlockSize the input block size of the TLS PRF hash algorithm.
* Used only for TLS 1.2+.
*
* @throws NullPointerException if keyAlg, label or seed is null
* @throws IllegalArgumentException if outputLength is negative or
* keyAlg is empty
*/
public TlsPrfParameterSpec(SecretKey secret, String keyAlg,
String label, byte[] seed, int outputLength,
String prfHashAlg, int prfHashLength, int prfBlockSize) {
if ((keyAlg == null) || (label == null) || (seed == null)) {
throw new NullPointerException(
"keyAlg, label or seed must not be null");
}
if (keyAlg.isEmpty()) {
throw new IllegalArgumentException("keyAlg can not be empty");
}
if (outputLength <= 0) {
throw new IllegalArgumentException("outputLength must be positive");
}
this.secret = secret;
this.keyAlg = keyAlg;
this.label = label;
this.seed = seed.clone();
this.outputLength = outputLength;
@ -87,6 +120,15 @@ public class TlsPrfParameterSpec implements AlgorithmParameterSpec {
this.prfBlockSize = prfBlockSize;
}
/**
* Returns the key algorithm name to use when generating the SecretKey.
*
* @return the key algorithm name
*/
public String getKeyAlg() {
return keyAlg;
}
/**
* Returns the secret to use in the PRF calculation, or null if there is no
* secret.

View File

@ -745,6 +745,12 @@ final class Finished {
"Failure to derive application secrets", gse);
}
// Calculate/save the exporter_master_secret. It uses
// the same handshakeHash as the client/server app traffic.
SecretKey exporterSecret = kd.deriveKey(
"TlsExporterMasterSecret");
chc.handshakeSession.setExporterMasterSecret(exporterSecret);
// The resumption master secret is stored in the session so
// it can be used after the handshake is completed.
SSLSecretDerivation sd = ((SSLSecretDerivation) kd).forContext(chc);
@ -1099,6 +1105,12 @@ final class Finished {
shc.baseReadSecret = readSecret;
shc.conContext.inputRecord.changeReadCiphers(readCipher);
// Calculate/save the exporter_master_secret. It uses
// the same handshakeHash as the client/server app traffic.
SecretKey exporterSecret = kd.deriveKey(
"TlsExporterMasterSecret");
shc.handshakeSession.setExporterMasterSecret(exporterSecret);
// The resumption master secret is stored in the session so
// it can be used after the handshake is completed.
shc.handshakeHash.update();

View File

@ -27,28 +27,32 @@ package sun.security.ssl;
import java.io.IOException;
import java.net.InetAddress;
import java.nio.ByteBuffer;
import java.security.Principal;
import java.security.PrivateKey;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.locks.ReentrantLock;
import java.util.zip.Adler32;
import javax.crypto.KDF;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.HKDFParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.net.ssl.ExtendedSSLSession;
import javax.net.ssl.SNIHostName;
import javax.net.ssl.SNIServerName;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSessionBindingEvent;
import javax.net.ssl.SSLSessionBindingListener;
import javax.net.ssl.SSLSessionContext;
import javax.net.ssl.*;
import sun.security.ssl.CipherSuite.HashAlg;
import sun.security.internal.spec.TlsPrfParameterSpec;
import static sun.security.ssl.CipherSuite.HashAlg.H_NONE;
import static sun.security.ssl.ProtocolVersion.*;
import sun.security.util.KeyUtil;
import sun.security.provider.X509Factory;
import sun.security.ssl.X509Authentication.X509Possession;
@ -99,6 +103,9 @@ final class SSLSessionImpl extends ExtendedSSLSession {
private Collection<SignatureScheme> peerSupportedSignAlgs; //for certificate
private boolean useDefaultPeerSignAlgs = false;
private List<byte[]> statusResponses;
private SecretKey exporterMasterSecret; // TLSv1.3+ exporter info
private RandomCookie clientRandom; // TLSv1.2- exporter info
private RandomCookie serverRandom;
private SecretKey resumptionMasterSecret;
private SecretKey preSharedKey;
private byte[] pskIdentity;
@ -612,6 +619,15 @@ final class SSLSessionImpl extends ExtendedSSLSession {
masterSecret = secret;
}
void setExporterMasterSecret(SecretKey secret) {
exporterMasterSecret = secret;
}
void setRandoms(RandomCookie client, RandomCookie server) {
clientRandom = client;
serverRandom = server;
}
void setResumptionMasterSecret(SecretKey secret) {
resumptionMasterSecret = secret;
}
@ -1397,6 +1413,269 @@ final class SSLSessionImpl extends ExtendedSSLSession {
return requestedServerNames;
}
/*
* keyAlg is used for switching between Keys/Data. If keyAlg is
* non-null, we are producing a Key, otherwise data.
*/
public Object exportKeyingMaterial(
String keyAlg, String label, byte[] context, int length)
throws SSLKeyException {
// Global preconditions
Objects.requireNonNull(label, "label can not be null");
if (length < 1) {
throw new IllegalArgumentException(
"length must be positive");
}
// Calculations are primarily based on protocol version.
if (protocolVersion.useTLS13PlusSpec()) {
// Unlikely, but check anyway.
if (exporterMasterSecret == null) {
throw new IllegalStateException(
"Exporter master secret not captured");
}
// TLS 1.3+ using HKDF-based calcs.
// TLS 1.3 (RFC 8446)
// Check the label/context lengths:
// struct {
// uint16 length = Length;
// opaque label<7..255> = "tls13 " + Label;
// opaque context<0..255> = Context;
// } HkdfLabel;
// label can have 249 bytes (+6 for "tls13 "), and context 255
// RFC 8446 allows for length of 2^16-1 (65536), but RFC 5869
// states:
//
// L length of output keying material in octets
// (<= 255*HashLen)
if (length > (255 * cipherSuite.hashAlg.hashLength )) {
throw new IllegalArgumentException(
"length is too large");
}
byte[] hkdfInfoLabel =
("tls13 " + label).getBytes(StandardCharsets.UTF_8);
if ((hkdfInfoLabel.length < 7) || hkdfInfoLabel.length > 255) {
throw new IllegalArgumentException(
"label length outside range");
}
// If no context (null) is provided, RFC 8446 requires an empty
// context be used, unlike RFC 5705.
context = (context != null ? context : new byte[0]);
if (context.length > 255) {
throw new IllegalArgumentException(
"context length outside range");
}
// Do RFC 8446:7.1-7.5 calculations
/*
* TLS-Exporter(label, context_value, key_length) =
* HKDF-Expand-Label(Derive-Secret(Secret, label, ""),
* "exporter", Hash(context_value), key_length)
*
* Derive-Secret(Secret, Label, Messages) =
* HKDF-Expand-Label(Secret, Label,
* Transcript-Hash(Messages), Hash.length)
*/
try {
// Use the ciphersuite's hashAlg for these calcs.
HashAlg hashAlg = cipherSuite.hashAlg;
KDF hkdf = KDF.getInstance(hashAlg.hkdfAlgorithm);
// First calculate the inner Derive-Secret(Secret, label, "")
MessageDigest md;
byte[] emptyHash;
// Create the "" digest...
try {
md = MessageDigest.getInstance(hashAlg.toString());
emptyHash = md.digest();
} catch (NoSuchAlgorithmException nsae) {
throw new ProviderException(
"Hash algorithm " + cipherSuite.hashAlg.name +
" is not available", nsae);
}
// ...then the hkdfInfo...
byte[] hkdfInfo = SSLSecretDerivation.createHkdfInfo(
hkdfInfoLabel, emptyHash, hashAlg.hashLength);
// ...then the "inner" HKDF-Expand-Label() to get the
// derivedSecret that is used as the Secret in the "outer"
// HKDF-Expand-Label().
SecretKey derivedSecret = hkdf.deriveKey("TlsKey",
HKDFParameterSpec.expandOnly(exporterMasterSecret,
hkdfInfo, hashAlg.hashLength));
try {
// Now do the "outer" HKDF-Expand-Label.
// HKDF-Expand-Label(derivedSecret, "exporter",
// Hash(context_value), key_length)
// If a context was supplied, use it, otherwise, use the
// previous hashed value of ""...
byte[] hash = ((context.length > 0) ?
md.digest(context) : emptyHash);
// ...now the hkdfInfo...
hkdfInfo = SSLSecretDerivation.createHkdfInfo(
("tls13 exporter").getBytes(StandardCharsets.UTF_8),
hash, length);
// ...now the final expand.
return ((keyAlg != null) ?
hkdf.deriveKey(keyAlg,
HKDFParameterSpec.expandOnly(derivedSecret,
hkdfInfo, length)) :
hkdf.deriveData(
HKDFParameterSpec.expandOnly(derivedSecret,
hkdfInfo, length)));
} finally {
KeyUtil.destroySecretKeys(derivedSecret);
}
} catch (Exception e) {
// For whatever reason, we couldn't generate. Wrap and return.
throw new SSLKeyException("Couldn't generate Exporter/HKDF", e);
}
} else if (protocolVersion.useTLS10PlusSpec()) {
// Unlikely, but check if randoms were not captured.
if (clientRandom == null || serverRandom == null) {
throw new IllegalStateException("Random nonces not captured");
}
// RFC 7505 using PRF-based calcs.
// TLS 1/1.1/1.2 (RFCs 2246/4346/5246) or
// DTLS 1.0/1.2 (RFCs 4347/6347)
// Note: In RFC 7627:
//
// If a client or server chooses to continue with a full handshake
// without the extended master secret extension ... they MUST NOT
// export any key material based on the new master secret for any
// subsequent application-level authentication ... it MUST
// disable [RFC5705] ...
if (!useExtendedMasterSecret) {
throw new SSLKeyException(
"Exporters require extended master secrets");
}
// Check for a "disambiguating label string" (i.e. non-empty).
// Don't see a max length restriction.
if (label.isEmpty()) {
throw new IllegalArgumentException(
"label length outside range");
}
// context length must fit in 2 unsigned bytes.
if ((context != null) && (context.length > 0xFFFF)) {
throw new IllegalArgumentException(
"Only 16-bit context lengths supported");
}
// Perform RFC 5705 calculations using the internal SunJCE PRF.
String prfAlg;
HashAlg hashAlg;
if (protocolVersion == TLS12) {
prfAlg = "SunTls12Prf";
hashAlg = cipherSuite.hashAlg;
} else { // all other cases
prfAlg = "SunTlsPrf";
hashAlg = H_NONE;
}
// Make a seed with randoms and optional context
// Note that if context is null, it is omitted from the calc
byte[] clientRandomBytes = clientRandom.randomBytes;
byte[] serverRandomBytes = serverRandom.randomBytes;
byte[] seed = new byte[
clientRandomBytes.length + serverRandomBytes.length +
((context != null) ? (2 + context.length) : 0)];
int pos = 0;
System.arraycopy(
clientRandomBytes, 0, seed, pos, clientRandomBytes.length);
pos += clientRandomBytes.length;
System.arraycopy(
serverRandomBytes, 0, seed, pos, serverRandomBytes.length);
pos += serverRandomBytes.length;
if (context != null) {
// RFC 5705, "If no context is provided, ..."
seed[pos++] = (byte) ((context.length >> 8) & 0xFF);
seed[pos++] = (byte) ((context.length) & 0xFF);
System.arraycopy(
context, 0, seed, pos, context.length);
}
// Call the PRF function.
try {
@SuppressWarnings("deprecation")
TlsPrfParameterSpec spec = new TlsPrfParameterSpec(
masterSecret, (keyAlg == null) ? "TlsKey" : keyAlg,
label, seed, length,
hashAlg.name, hashAlg.hashLength, hashAlg.blockSize);
KeyGenerator kg = KeyGenerator.getInstance(prfAlg);
kg.init(spec);
SecretKey key = kg.generateKey();
if (keyAlg != null) {
return key;
} else {
byte[] b = key.getEncoded();
if (b == null) {
throw new UnsupportedOperationException(
"Could not extract encoding from SecretKey");
}
return b;
}
} catch (NoSuchAlgorithmException |
InvalidAlgorithmParameterException e) {
throw new SSLKeyException("Could not generate Exporter/PRF", e);
}
} else {
// SSLv3 is vulnerable to a triple handshake attack and can't be
// mitigated by RFC 7627. Don't support this or any other
// unknown protocol.
throw new SSLKeyException(
"Exporters not supported in " + protocolVersion);
}
}
/**
* Generate Exported Key Material (EKM) calculated according to the
* algorithms defined in RFCs 5705/8446.
*/
@Override
public SecretKey exportKeyingMaterialKey(String keyAlg,
String label, byte[] context, int length) throws SSLKeyException {
Objects.requireNonNull(keyAlg, "keyAlg can not be null");
if (keyAlg.isEmpty()) {
throw new IllegalArgumentException(
"keyAlg is empty");
}
return (SecretKey) exportKeyingMaterial(keyAlg, label, context,
length);
}
/**
* Generate Exported Key Material (EKM) calculated according to the
* algorithms defined in RFCs 5705/8446.
*/
@Override
public byte[] exportKeyingMaterialData(
String label, byte[] context, int length) throws SSLKeyException {
return (byte[])exportKeyingMaterial(null, label, context, length);
}
/** Returns a string representation of this SSL session */
@Override
public String toString() {

View File

@ -356,6 +356,9 @@ final class ServerHello {
clientHello);
shc.serverHelloRandom = shm.serverRandom;
shc.handshakeSession.setRandoms(shc.clientHelloRandom,
shc.serverHelloRandom);
// Produce extensions for ServerHello handshake message.
SSLExtension[] serverHelloExtensions =
shc.sslConfig.getEnabledExtensions(
@ -1129,6 +1132,9 @@ final class ServerHello {
chc.sslConfig.maximumPacketSize);
}
chc.handshakeSession.setRandoms(chc.clientHelloRandom,
chc.serverHelloRandom);
//
// update
//

View File

@ -276,6 +276,7 @@ final class P11SecretKeyFactory extends SecretKeyFactorySpi {
putKeyInfo(new TLSKeyInfo("TlsClientAppTrafficSecret"));
putKeyInfo(new TLSKeyInfo("TlsClientHandshakeTrafficSecret"));
putKeyInfo(new TLSKeyInfo("TlsEarlySecret"));
putKeyInfo(new TLSKeyInfo("TlsExporterMasterSecret"));
putKeyInfo(new TLSKeyInfo("TlsFinishedSecret"));
putKeyInfo(new TLSKeyInfo("TlsHandshakeSecret"));
putKeyInfo(new TLSKeyInfo("TlsKey"));

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2005, 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2005, 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
@ -155,7 +155,7 @@ final class P11TlsPrfGenerator extends KeyGeneratorSpi {
token.p11.C_SignUpdate(session.id(), 0, seed, 0, seed.length);
byte[] out = token.p11.C_SignFinal
(session.id(), spec.getOutputLength());
return new SecretKeySpec(out, "TlsPrf");
return new SecretKeySpec(out, spec.getKeyAlg());
} catch (PKCS11Exception e) {
throw new ProviderException("Could not calculate PRF", e);
} finally {
@ -181,7 +181,7 @@ final class P11TlsPrfGenerator extends KeyGeneratorSpi {
token.p11.C_SignUpdate(session.id(), 0, seed, 0, seed.length);
byte[] out = token.p11.C_SignFinal
(session.id(), spec.getOutputLength());
return new SecretKeySpec(out, "TlsPrf");
return new SecretKeySpec(out, spec.getKeyAlg());
} catch (PKCS11Exception e) {
throw new ProviderException("Could not calculate PRF", e);
} finally {
@ -201,7 +201,7 @@ final class P11TlsPrfGenerator extends KeyGeneratorSpi {
session = token.getOpSession();
token.p11.C_DeriveKey(session.id(),
new CK_MECHANISM(mechanism, params), keyID, null);
return new SecretKeySpec(out, "TlsPrf");
return new SecretKeySpec(out, spec.getKeyAlg());
} catch (PKCS11Exception e) {
throw new ProviderException("Could not calculate PRF", e);
} finally {

View File

@ -0,0 +1,573 @@
/*
* 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());
}
}