diff --git a/src/java.base/share/classes/com/sun/crypto/provider/TlsPrfGenerator.java b/src/java.base/share/classes/com/sun/crypto/provider/TlsPrfGenerator.java index f8f733b6d2f..6ca019ae87b 100644 --- a/src/java.base/share/classes/com/sun/crypto/provider/TlsPrfGenerator.java +++ b/src/java.base/share/classes/com/sun/crypto/provider/TlsPrfGenerator.java @@ -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); } diff --git a/src/java.base/share/classes/javax/net/ssl/ExtendedSSLSession.java b/src/java.base/share/classes/javax/net/ssl/ExtendedSSLSession.java index 2b98f4845cf..980ff77d83a 100644 --- a/src/java.base/share/classes/javax/net/ssl/ExtendedSSLSession.java +++ b/src/java.base/share/classes/javax/net/ssl/ExtendedSSLSession.java @@ -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 getStatusResponses() { throw new UnsupportedOperationException(); } + + /** + * Generates Exported Keying Material (EKM) calculated according to the + * algorithms defined in RFCs 5705/8446. + *

+ * 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. + *

+ * {@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 + * + * Java Security Standard Algorithm Names Specification + * 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. + *

+ * 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. + *

+ * {@code label} will be converted to bytes using + * the {@link java.nio.charset.StandardCharsets#UTF_8} + * character encoding. + *

+ * 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"); + } } diff --git a/src/java.base/share/classes/sun/security/internal/spec/TlsPrfParameterSpec.java b/src/java.base/share/classes/sun/security/internal/spec/TlsPrfParameterSpec.java index f87579ebb70..14abf9f30dc 100644 --- a/src/java.base/share/classes/sun/security/internal/spec/TlsPrfParameterSpec.java +++ b/src/java.base/share/classes/sun/security/internal/spec/TlsPrfParameterSpec.java @@ -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. diff --git a/src/java.base/share/classes/sun/security/ssl/Finished.java b/src/java.base/share/classes/sun/security/ssl/Finished.java index dcb499b6d0c..e388672cb7f 100644 --- a/src/java.base/share/classes/sun/security/ssl/Finished.java +++ b/src/java.base/share/classes/sun/security/ssl/Finished.java @@ -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(); diff --git a/src/java.base/share/classes/sun/security/ssl/SSLSessionImpl.java b/src/java.base/share/classes/sun/security/ssl/SSLSessionImpl.java index 032aa7de5b3..5eb9f72af46 100644 --- a/src/java.base/share/classes/sun/security/ssl/SSLSessionImpl.java +++ b/src/java.base/share/classes/sun/security/ssl/SSLSessionImpl.java @@ -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 peerSupportedSignAlgs; //for certificate private boolean useDefaultPeerSignAlgs = false; private List 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() { diff --git a/src/java.base/share/classes/sun/security/ssl/ServerHello.java b/src/java.base/share/classes/sun/security/ssl/ServerHello.java index e38022f1c3e..d092d6c07de 100644 --- a/src/java.base/share/classes/sun/security/ssl/ServerHello.java +++ b/src/java.base/share/classes/sun/security/ssl/ServerHello.java @@ -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 // diff --git a/src/jdk.crypto.cryptoki/share/classes/sun/security/pkcs11/P11SecretKeyFactory.java b/src/jdk.crypto.cryptoki/share/classes/sun/security/pkcs11/P11SecretKeyFactory.java index b80ccc35352..a2c7a072769 100644 --- a/src/jdk.crypto.cryptoki/share/classes/sun/security/pkcs11/P11SecretKeyFactory.java +++ b/src/jdk.crypto.cryptoki/share/classes/sun/security/pkcs11/P11SecretKeyFactory.java @@ -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")); diff --git a/src/jdk.crypto.cryptoki/share/classes/sun/security/pkcs11/P11TlsPrfGenerator.java b/src/jdk.crypto.cryptoki/share/classes/sun/security/pkcs11/P11TlsPrfGenerator.java index 32f99b3fd5a..f6691a3d279 100644 --- a/src/jdk.crypto.cryptoki/share/classes/sun/security/pkcs11/P11TlsPrfGenerator.java +++ b/src/jdk.crypto.cryptoki/share/classes/sun/security/pkcs11/P11TlsPrfGenerator.java @@ -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 { diff --git a/test/jdk/javax/net/ssl/ExtendedSSLSession/ExportKeyingMaterialTests.java b/test/jdk/javax/net/ssl/ExtendedSSLSession/ExportKeyingMaterialTests.java new file mode 100644 index 00000000000..77d47d07475 --- /dev/null +++ b/test/jdk/javax/net/ssl/ExtendedSSLSession/ExportKeyingMaterialTests.java @@ -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. + *

+ * The test creates two SSLEngines, simulating a client and server. + * The "transport" layer consists two byte buffers: think of them + * as directly connected pipes. + *

+ * 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. + *

+ * 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()); + } +}