8247630: Use two key share entries
Reviewed-by: xuelei
This commit is contained in:
parent
f2e69156c8
commit
2aa291ad2c
@ -31,12 +31,15 @@ import java.security.GeneralSecurityException;
|
|||||||
import java.text.MessageFormat;
|
import java.text.MessageFormat;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.EnumSet;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.ListIterator;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import javax.net.ssl.SSLProtocolException;
|
import javax.net.ssl.SSLProtocolException;
|
||||||
import sun.security.ssl.KeyShareExtension.CHKeyShareSpec;
|
import sun.security.ssl.KeyShareExtension.CHKeyShareSpec;
|
||||||
|
import sun.security.ssl.NamedGroup.NamedGroupSpec;
|
||||||
import sun.security.ssl.SSLExtension.ExtensionConsumer;
|
import sun.security.ssl.SSLExtension.ExtensionConsumer;
|
||||||
import sun.security.ssl.SSLExtension.SSLExtensionSpec;
|
import sun.security.ssl.SSLExtension.SSLExtensionSpec;
|
||||||
import sun.security.ssl.SSLHandshake.HandshakeMessage;
|
import sun.security.ssl.SSLHandshake.HandshakeMessage;
|
||||||
@ -248,33 +251,23 @@ final class KeyShareExtension {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Go through the named groups and take the most-preferred
|
||||||
|
// group from two categories (i.e. XDH and ECDHE). Once we have
|
||||||
|
// the most preferred group from two types we can exit the loop.
|
||||||
List<KeyShareEntry> keyShares = new LinkedList<>();
|
List<KeyShareEntry> keyShares = new LinkedList<>();
|
||||||
|
EnumSet<NamedGroupSpec> ngTypes =
|
||||||
|
EnumSet.noneOf(NamedGroupSpec.class);
|
||||||
|
byte[] keyExchangeData;
|
||||||
for (NamedGroup ng : namedGroups) {
|
for (NamedGroup ng : namedGroups) {
|
||||||
SSLKeyExchange ke = SSLKeyExchange.valueOf(ng);
|
if (!ngTypes.contains(ng.spec)) {
|
||||||
if (ke == null) {
|
if ((keyExchangeData = getShare(chc, ng)) != null) {
|
||||||
if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
|
keyShares.add(new KeyShareEntry(ng.id,
|
||||||
SSLLogger.warning(
|
keyExchangeData));
|
||||||
"No key exchange for named group " + ng.name);
|
ngTypes.add(ng.spec);
|
||||||
|
if (ngTypes.size() == 2) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
SSLPossession[] poses = ke.createPossessions(chc);
|
|
||||||
for (SSLPossession pos : poses) {
|
|
||||||
// update the context
|
|
||||||
chc.handshakePossessions.add(pos);
|
|
||||||
if (!(pos instanceof NamedGroupPossession)) {
|
|
||||||
// May need more possesion types in the future.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
keyShares.add(new KeyShareEntry(ng.id, pos.encode()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// One key share entry only. Too much key share entries makes
|
|
||||||
// the ClientHello handshake message really big.
|
|
||||||
if (!keyShares.isEmpty()) {
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -295,6 +288,29 @@ final class KeyShareExtension {
|
|||||||
|
|
||||||
return extData;
|
return extData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static byte[] getShare(ClientHandshakeContext chc,
|
||||||
|
NamedGroup ng) {
|
||||||
|
byte[] share = null;
|
||||||
|
SSLKeyExchange ke = SSLKeyExchange.valueOf(ng);
|
||||||
|
if (ke == null) {
|
||||||
|
if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
|
||||||
|
SSLLogger.warning(
|
||||||
|
"No key exchange for named group " + ng.name);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
SSLPossession[] poses = ke.createPossessions(chc);
|
||||||
|
for (SSLPossession pos : poses) {
|
||||||
|
// update the context
|
||||||
|
chc.handshakePossessions.add(pos);
|
||||||
|
// May need more possesion types in the future.
|
||||||
|
if (pos instanceof NamedGroupPossession) {
|
||||||
|
return pos.encode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return share;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -873,11 +889,33 @@ final class KeyShareExtension {
|
|||||||
NamedGroup.nameOf(spec.selectedGroup));
|
NamedGroup.nameOf(spec.selectedGroup));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The server-selected named group from a HelloRetryRequest must
|
||||||
|
// meet the following criteria:
|
||||||
|
// 1. It must be one of the named groups in the supported_groups
|
||||||
|
// extension in the client hello.
|
||||||
|
// 2. It cannot be one of the groups in the key_share extension
|
||||||
|
// from the client hello.
|
||||||
if (!chc.clientRequestedNamedGroups.contains(serverGroup)) {
|
if (!chc.clientRequestedNamedGroups.contains(serverGroup)) {
|
||||||
throw chc.conContext.fatal(Alert.ILLEGAL_PARAMETER,
|
throw chc.conContext.fatal(Alert.ILLEGAL_PARAMETER,
|
||||||
"Unexpected HelloRetryRequest selected group: " +
|
"Unexpected HelloRetryRequest selected group: " +
|
||||||
serverGroup.name);
|
serverGroup.name);
|
||||||
}
|
}
|
||||||
|
CHKeyShareSpec chKsSpec = (CHKeyShareSpec)
|
||||||
|
chc.handshakeExtensions.get(SSLExtension.CH_KEY_SHARE);
|
||||||
|
if (chKsSpec != null) {
|
||||||
|
for (KeyShareEntry kse : chKsSpec.clientShares) {
|
||||||
|
if (serverGroup.id == kse.namedGroupId) {
|
||||||
|
throw chc.conContext.fatal(Alert.ILLEGAL_PARAMETER,
|
||||||
|
"Illegal HelloRetryRequest selected group: " +
|
||||||
|
serverGroup.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Something has gone very wrong if we're here.
|
||||||
|
throw chc.conContext.fatal(Alert.INTERNAL_ERROR,
|
||||||
|
"Unable to retrieve ClientHello key_share extension " +
|
||||||
|
"during HRR processing");
|
||||||
|
}
|
||||||
|
|
||||||
// update the context
|
// update the context
|
||||||
|
|
||||||
|
276
test/jdk/javax/net/ssl/TLSv13/ClientHelloKeyShares.java
Normal file
276
test/jdk/javax/net/ssl/TLSv13/ClientHelloKeyShares.java
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2020, 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. For further debugging output
|
||||||
|
// set the -Djavax.net.debug=ssl:handshake property on the @run lines.
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @test
|
||||||
|
* @bug 8247630
|
||||||
|
* @summary Use two key share entries
|
||||||
|
* @run main/othervm ClientHelloKeyShares 29 23
|
||||||
|
* @run main/othervm -Djdk.tls.namedGroups=secp384r1,secp521r1,x448,ffdhe2048 ClientHelloKeyShares 24 30
|
||||||
|
* @run main/othervm -Djdk.tls.namedGroups=sect163k1,sect163r1,x25519 ClientHelloKeyShares 29
|
||||||
|
* @run main/othervm -Djdk.tls.namedGroups=sect163k1,sect163r1,secp256r1 ClientHelloKeyShares 23
|
||||||
|
* @run main/othervm -Djdk.tls.namedGroups=sect163k1,sect163r1,ffdhe2048,ffdhe3072,ffdhe4096 ClientHelloKeyShares 256
|
||||||
|
* @run main/othervm -Djdk.tls.namedGroups=sect163k1,ffdhe2048,x25519,secp256r1 ClientHelloKeyShares 256 29
|
||||||
|
* @run main/othervm -Djdk.tls.namedGroups=secp256r1,secp384r1,ffdhe2048,x25519 ClientHelloKeyShares 23 256
|
||||||
|
*/
|
||||||
|
|
||||||
|
import javax.net.ssl.*;
|
||||||
|
import javax.net.ssl.SSLEngineResult.*;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
|
||||||
|
public class ClientHelloKeyShares {
|
||||||
|
|
||||||
|
// Some TLS constants we'll use for testing
|
||||||
|
private static final int TLS_REC_HANDSHAKE = 22;
|
||||||
|
private static final int HELLO_EXT_SUPP_GROUPS = 10;
|
||||||
|
private static final int HELLO_EXT_SUPP_VERS = 43;
|
||||||
|
private static final int HELLO_EXT_KEY_SHARE = 51;
|
||||||
|
private static final int TLS_PROT_VER_13 = 0x0304;
|
||||||
|
private static final int NG_SECP256R1 = 0x0017;
|
||||||
|
private static final int NG_SECP384R1 = 0x0018;
|
||||||
|
private static final int NG_X25519 = 0x001D;
|
||||||
|
private static final int NG_X448 = 0x001E;
|
||||||
|
|
||||||
|
public static void main(String args[]) throws Exception {
|
||||||
|
// Arguments to this test are an abitrary number of integer
|
||||||
|
// values which will be the expected NamedGroup IDs in the key_share
|
||||||
|
// extension. Expected named group assertions may also be affected
|
||||||
|
// by setting the jdk.tls.namedGroups System property.
|
||||||
|
List<Integer> expectedKeyShares = new ArrayList<>();
|
||||||
|
Arrays.stream(args).forEach(arg ->
|
||||||
|
expectedKeyShares.add(Integer.valueOf(arg)));
|
||||||
|
|
||||||
|
SSLContext sslCtx = SSLContext.getDefault();
|
||||||
|
SSLEngine engine = sslCtx.createSSLEngine();
|
||||||
|
engine.setUseClientMode(true);
|
||||||
|
SSLSession session = engine.getSession();
|
||||||
|
ByteBuffer clientOut = ByteBuffer.wrap("I'm a Client".getBytes());
|
||||||
|
ByteBuffer cTOs =
|
||||||
|
ByteBuffer.allocateDirect(session.getPacketBufferSize());
|
||||||
|
|
||||||
|
// Create and check the ClientHello message
|
||||||
|
SSLEngineResult clientResult = engine.wrap(clientOut, cTOs);
|
||||||
|
logResult("client wrap: ", clientResult);
|
||||||
|
if (clientResult.getStatus() != SSLEngineResult.Status.OK) {
|
||||||
|
throw new RuntimeException("Client wrap got status: " +
|
||||||
|
clientResult.getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
cTOs.flip();
|
||||||
|
System.out.println(dumpHexBytes(cTOs));
|
||||||
|
checkClientHello(cTOs, expectedKeyShares);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void logResult(String str, SSLEngineResult result) {
|
||||||
|
HandshakeStatus hsStatus = result.getHandshakeStatus();
|
||||||
|
System.out.println(str +
|
||||||
|
result.getStatus() + "/" + hsStatus + ", " +
|
||||||
|
result.bytesConsumed() + "/" + result.bytesProduced() +
|
||||||
|
" bytes");
|
||||||
|
if (hsStatus == HandshakeStatus.FINISHED) {
|
||||||
|
System.out.println("\t...ready for application data");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dump a ByteBuffer as a hexdump to stdout. The dumping routine will
|
||||||
|
* start at the current position of the buffer and run to its limit.
|
||||||
|
* After completing the dump, the position will be returned to its
|
||||||
|
* starting point.
|
||||||
|
*
|
||||||
|
* @param data the ByteBuffer to dump to stdout.
|
||||||
|
*
|
||||||
|
* @return the hexdump of the byte array.
|
||||||
|
*/
|
||||||
|
private static String dumpHexBytes(ByteBuffer data) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
if (data != null) {
|
||||||
|
int i = 0;
|
||||||
|
data.mark();
|
||||||
|
while (data.hasRemaining()) {
|
||||||
|
if (i % 16 == 0 && i != 0) {
|
||||||
|
sb.append("\n");
|
||||||
|
}
|
||||||
|
sb.append(String.format("%02X ", data.get()));
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
data.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests the ClientHello for the presence of the key shares in the supplied
|
||||||
|
* List of key share identifiers.
|
||||||
|
*
|
||||||
|
* @param data the ByteBuffer containing the ClientHello bytes
|
||||||
|
* @param keyShareTypes a List containing the expected key shares
|
||||||
|
*
|
||||||
|
* @throws RuntimeException if there is a deviation between what is expected
|
||||||
|
* and what is supplied. It will also throw this exception if other
|
||||||
|
* basic structural elements of the ClientHello are not found (e.g. TLS 1.3
|
||||||
|
* is not in the list of supported groups, etc.)
|
||||||
|
*/
|
||||||
|
private static void checkClientHello(ByteBuffer data,
|
||||||
|
List<Integer> expectedKeyShares) {
|
||||||
|
Objects.requireNonNull(data);
|
||||||
|
data.mark();
|
||||||
|
|
||||||
|
// Process the TLS record header
|
||||||
|
int type = Byte.toUnsignedInt(data.get());
|
||||||
|
int ver_major = Byte.toUnsignedInt(data.get());
|
||||||
|
int ver_minor = Byte.toUnsignedInt(data.get());
|
||||||
|
int recLen = Short.toUnsignedInt(data.getShort());
|
||||||
|
|
||||||
|
// Simple sanity checks
|
||||||
|
if (type != 22) {
|
||||||
|
throw new RuntimeException("Not a handshake: Type = " + type);
|
||||||
|
} else if (recLen > data.remaining()) {
|
||||||
|
throw new RuntimeException("Incomplete record in buffer: " +
|
||||||
|
"Record length = " + recLen + ", Remaining = " +
|
||||||
|
data.remaining());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grab the handshake message header.
|
||||||
|
int msgHdr = data.getInt();
|
||||||
|
int msgType = (msgHdr >> 24) & 0x000000FF;
|
||||||
|
int msgLen = msgHdr & 0x00FFFFFF;
|
||||||
|
|
||||||
|
// More simple sanity checks
|
||||||
|
if (msgType != 1) {
|
||||||
|
throw new RuntimeException("Not a ClientHello: Type = " + msgType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip over the protocol version and client random
|
||||||
|
data.position(data.position() + 34);
|
||||||
|
|
||||||
|
// Jump past the session ID (if there is one)
|
||||||
|
int sessLen = Byte.toUnsignedInt(data.get());
|
||||||
|
if (sessLen != 0) {
|
||||||
|
data.position(data.position() + sessLen);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jump past the cipher suites
|
||||||
|
int csLen = Short.toUnsignedInt(data.getShort());
|
||||||
|
if (csLen != 0) {
|
||||||
|
data.position(data.position() + csLen);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...and the compression
|
||||||
|
int compLen = Byte.toUnsignedInt(data.get());
|
||||||
|
if (compLen != 0) {
|
||||||
|
data.position(data.position() + compLen);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now for the fun part. Go through the extensions and look
|
||||||
|
// for supported_versions (to make sure TLS 1.3 is asserted) and
|
||||||
|
// the expected key shares are present.
|
||||||
|
boolean foundSupVer = false;
|
||||||
|
boolean foundKeyShare = false;
|
||||||
|
int extsLen = Short.toUnsignedInt(data.getShort());
|
||||||
|
List<Integer> supGrpList = new ArrayList<>();
|
||||||
|
List<Integer> chKeyShares = new ArrayList<>();
|
||||||
|
while (data.hasRemaining()) {
|
||||||
|
int extType = Short.toUnsignedInt(data.getShort());
|
||||||
|
int extLen = Short.toUnsignedInt(data.getShort());
|
||||||
|
boolean foundTLS13 = false;
|
||||||
|
switch (extType) {
|
||||||
|
case HELLO_EXT_SUPP_GROUPS:
|
||||||
|
int supGrpLen = Short.toUnsignedInt(data.getShort());
|
||||||
|
for (int remain = supGrpLen; remain > 0; remain -= 2) {
|
||||||
|
supGrpList.add(Short.toUnsignedInt(data.getShort()));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case HELLO_EXT_SUPP_VERS:
|
||||||
|
foundSupVer = true;
|
||||||
|
int supVerLen = Byte.toUnsignedInt(data.get());
|
||||||
|
for (int remain = supVerLen; remain > 0; remain -= 2) {
|
||||||
|
foundTLS13 |= (Short.toUnsignedInt(data.getShort()) ==
|
||||||
|
TLS_PROT_VER_13);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundTLS13) {
|
||||||
|
throw new RuntimeException("Missing TLS 1.3 Protocol " +
|
||||||
|
"Version in supported_groups");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case HELLO_EXT_KEY_SHARE:
|
||||||
|
foundKeyShare = true;
|
||||||
|
int ksListLen = Short.toUnsignedInt(data.getShort());
|
||||||
|
while (ksListLen > 0) {
|
||||||
|
chKeyShares.add(Short.toUnsignedInt(data.getShort()));
|
||||||
|
int ksLen = Short.toUnsignedInt(data.getShort());
|
||||||
|
data.position(data.position() + ksLen);
|
||||||
|
ksListLen -= (4 + ksLen);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
data.position(data.position() + extLen);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We must have parsed supported_versions, key_share and
|
||||||
|
// supported_groups extensions.
|
||||||
|
if ((foundSupVer && foundKeyShare && !supGrpList.isEmpty()) == false) {
|
||||||
|
throw new RuntimeException("Missing one or more of key_share, " +
|
||||||
|
"supported_versions and/or supported_groups extensions");
|
||||||
|
}
|
||||||
|
|
||||||
|
// The key share types we expected in the test should match exactly what
|
||||||
|
// was asserted in the client hello
|
||||||
|
if (!expectedKeyShares.equals(chKeyShares)) {
|
||||||
|
StringBuilder sb = new StringBuilder(
|
||||||
|
"Expected and Actual key_share lists differ: ");
|
||||||
|
sb.append("Expected: ");
|
||||||
|
expectedKeyShares.forEach(ng -> sb.append(ng).append(" "));
|
||||||
|
sb.append(", Actual: ");
|
||||||
|
chKeyShares.forEach(ng -> sb.append(ng).append(" "));
|
||||||
|
throw new RuntimeException(sb.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// The order of the key shares should match the order of precedence
|
||||||
|
// of the same named groups asserted in the supported_groups extension.
|
||||||
|
// (RFC 8446, 4.2.8)
|
||||||
|
int prevChNg = -1;
|
||||||
|
for (int ng : chKeyShares) {
|
||||||
|
int chNgPos = supGrpList.indexOf(ng);
|
||||||
|
if (chNgPos <= prevChNg) {
|
||||||
|
throw new RuntimeException("Order of precedence violation " +
|
||||||
|
"for NamedGroup " + ng + " between key_share and " +
|
||||||
|
"supported_groups extensions");
|
||||||
|
}
|
||||||
|
prevChNg = chNgPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should be at the end of the ClientHello
|
||||||
|
data.reset();
|
||||||
|
}
|
||||||
|
}
|
435
test/jdk/javax/net/ssl/TLSv13/HRRKeyShares.java
Normal file
435
test/jdk/javax/net/ssl/TLSv13/HRRKeyShares.java
Normal file
@ -0,0 +1,435 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2020, 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. For further debugging output
|
||||||
|
// set the -Djavax.net.debug=ssl:handshake property on the @run lines.
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @test
|
||||||
|
* @bug 8247630
|
||||||
|
* @summary Use two key share entries
|
||||||
|
* @library /test/lib
|
||||||
|
* @run main/othervm -Djdk.tls.namedGroups=x25519,secp256r1,secp384r1 HRRKeyShares
|
||||||
|
*/
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import javax.net.ssl.*;
|
||||||
|
import javax.net.ssl.SSLEngineResult.*;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import jdk.test.lib.Utils;
|
||||||
|
|
||||||
|
|
||||||
|
public class HRRKeyShares {
|
||||||
|
|
||||||
|
// Some TLS constants we'll use for testing
|
||||||
|
private static final int TLS_REC_HANDSHAKE = 22;
|
||||||
|
private static final int TLS_REC_ALERT = 21;
|
||||||
|
private static final int HS_MSG_CLIHELLO = 1;
|
||||||
|
private static final int HS_MSG_SERVHELLO = 2; // Also for HRR
|
||||||
|
private static final int HELLO_EXT_SUPP_GROUPS = 10;
|
||||||
|
private static final int HELLO_EXT_SUPP_VERS = 43;
|
||||||
|
private static final int HELLO_EXT_KEY_SHARE = 51;
|
||||||
|
private static final int TLS_LEGACY_VER = 0x0303; // TLSv1.2
|
||||||
|
private static final int TLS_PROT_VER_13 = 0x0304; // TLSv1.3
|
||||||
|
private static final int NG_SECP256R1 = 0x0017;
|
||||||
|
private static final int NG_SECP384R1 = 0x0018;
|
||||||
|
private static final int NG_X25519 = 0x001D;
|
||||||
|
private static final int NG_X448 = 0x001E;
|
||||||
|
private static final int NG_GC512A = 0x0026;
|
||||||
|
private static final int COMP_NONE = 0;
|
||||||
|
private static final int ALERT_TYPE_FATAL = 2;
|
||||||
|
private static final int ALERT_DESC_ILLEGAL_PARAM = 47;
|
||||||
|
private static final byte[] HRR_RANDOM = Utils.toByteArray(
|
||||||
|
"CF21AD74E59A6111BE1D8C021E65B891" +
|
||||||
|
"C2A211167ABB8C5E079E09E2C8A8339C");
|
||||||
|
|
||||||
|
static class ClientHello {
|
||||||
|
// TLS Record header fields
|
||||||
|
final int recType;
|
||||||
|
final int recVers;
|
||||||
|
final int recLength;
|
||||||
|
|
||||||
|
// Handshake header fields
|
||||||
|
final int hsMsgType;
|
||||||
|
final int hsMsgLength;
|
||||||
|
|
||||||
|
// ClientHello fields
|
||||||
|
final int version;
|
||||||
|
final byte[] random;
|
||||||
|
final byte[] sessId;
|
||||||
|
final List<Integer> cipherSuites = new ArrayList<>();
|
||||||
|
final List<Integer> compressionList = new ArrayList<>();
|
||||||
|
final Map<Integer,byte[]> extensionMap = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
// These are fields built from specific extension data fields we
|
||||||
|
// are interested in for our tests
|
||||||
|
final List<Integer> suppGroups = new ArrayList<>();
|
||||||
|
final Map<Integer,byte[]> keyShares = new LinkedHashMap<>();
|
||||||
|
final List<Integer> suppVersions = new ArrayList<>();
|
||||||
|
|
||||||
|
ClientHello(ByteBuffer data) {
|
||||||
|
Objects.requireNonNull(data);
|
||||||
|
data.mark();
|
||||||
|
|
||||||
|
// Process the TLS record header
|
||||||
|
recType = Byte.toUnsignedInt(data.get());
|
||||||
|
recVers = Short.toUnsignedInt(data.getShort());
|
||||||
|
recLength = Short.toUnsignedInt(data.getShort());
|
||||||
|
if (recType != TLS_REC_HANDSHAKE) {
|
||||||
|
throw new RuntimeException("Not a Handshake TLS record. " +
|
||||||
|
"Type = " + recType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the Handshake message header
|
||||||
|
int recHdr = data.getInt();
|
||||||
|
hsMsgType = recHdr >>> 24;
|
||||||
|
hsMsgLength = recHdr & 0x00FFFFFF;
|
||||||
|
if (hsMsgType != HS_MSG_CLIHELLO) {
|
||||||
|
throw new RuntimeException("Not a ClientHello message. " +
|
||||||
|
"Type = " + hsMsgType);
|
||||||
|
} else if (hsMsgLength > data.remaining()) {
|
||||||
|
throw new RuntimeException("Incomplete record in buffer: " +
|
||||||
|
"Record length = " + hsMsgLength + ", Remaining = " +
|
||||||
|
data.remaining());
|
||||||
|
}
|
||||||
|
|
||||||
|
version = Short.toUnsignedInt(data.getShort());
|
||||||
|
random = new byte[32];
|
||||||
|
data.get(random);
|
||||||
|
sessId = new byte[Byte.toUnsignedInt(data.get())];
|
||||||
|
data.get(sessId);
|
||||||
|
|
||||||
|
int suiteLen = Short.toUnsignedInt(data.getShort());
|
||||||
|
while (suiteLen > 0) {
|
||||||
|
cipherSuites.add(Short.toUnsignedInt(data.getShort()));
|
||||||
|
suiteLen -= 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
int compLen = Byte.toUnsignedInt(data.get());
|
||||||
|
while (compLen > 0) {
|
||||||
|
compressionList.add(Byte.toUnsignedInt(data.get()));
|
||||||
|
compLen--;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extension processing time!
|
||||||
|
int extListLen = Short.toUnsignedInt(data.getShort());
|
||||||
|
while (extListLen > 0) {
|
||||||
|
int extType = Short.toUnsignedInt(data.getShort());
|
||||||
|
int extLen = Short.toUnsignedInt(data.getShort());
|
||||||
|
byte[] extData = new byte[extLen];
|
||||||
|
data.get(extData);
|
||||||
|
extensionMap.put(extType, extData);
|
||||||
|
switch (extType) {
|
||||||
|
case HELLO_EXT_SUPP_GROUPS:
|
||||||
|
ByteBuffer sgBuf = ByteBuffer.wrap(extData);
|
||||||
|
int supGrpLen = Short.toUnsignedInt(sgBuf.getShort());
|
||||||
|
for (int remain = supGrpLen; remain > 0; remain -= 2) {
|
||||||
|
suppGroups.add(Short.toUnsignedInt(
|
||||||
|
sgBuf.getShort()));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case HELLO_EXT_SUPP_VERS:
|
||||||
|
ByteBuffer svBuf = ByteBuffer.wrap(extData);
|
||||||
|
int supVerLen = Byte.toUnsignedInt(svBuf.get());
|
||||||
|
for (int remain = supVerLen; remain > 0; remain -= 2) {
|
||||||
|
suppVersions.add(Short.toUnsignedInt(
|
||||||
|
svBuf.getShort()));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case HELLO_EXT_KEY_SHARE:
|
||||||
|
ByteBuffer ksBuf = ByteBuffer.wrap(extData);
|
||||||
|
int ksListLen = Short.toUnsignedInt(ksBuf.getShort());
|
||||||
|
while (ksListLen > 0) {
|
||||||
|
int namedGroup = Short.toUnsignedInt(
|
||||||
|
ksBuf.getShort());
|
||||||
|
int ksLen = Short.toUnsignedInt(ksBuf.getShort());
|
||||||
|
byte[] ksData = new byte[ksLen];
|
||||||
|
ksBuf.get(ksData);
|
||||||
|
keyShares.put(namedGroup, ksData);
|
||||||
|
ksListLen -= (4 + ksLen);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
extListLen -= (4 + extLen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Alert {
|
||||||
|
final int recType;
|
||||||
|
final int recVers;
|
||||||
|
final int recLength;
|
||||||
|
final int alertType;
|
||||||
|
final int alertDesc;
|
||||||
|
|
||||||
|
Alert(ByteBuffer data) {
|
||||||
|
Objects.requireNonNull(data);
|
||||||
|
data.mark();
|
||||||
|
|
||||||
|
// Process the TLS record header
|
||||||
|
recType = Byte.toUnsignedInt(data.get());
|
||||||
|
recVers = Short.toUnsignedInt(data.getShort());
|
||||||
|
recLength = Short.toUnsignedInt(data.getShort());
|
||||||
|
if (recType != TLS_REC_ALERT) {
|
||||||
|
throw new RuntimeException("Not a Handshake TLS record. " +
|
||||||
|
"Type = " + recType);
|
||||||
|
}
|
||||||
|
|
||||||
|
alertType = Byte.toUnsignedInt(data.get());
|
||||||
|
alertDesc = Byte.toUnsignedInt(data.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String args[]) throws Exception {
|
||||||
|
System.out.println("Test 1: Good HRR exchange using secp384r1");
|
||||||
|
hrrKeyShareTest(NG_SECP384R1, true);
|
||||||
|
System.out.println();
|
||||||
|
|
||||||
|
System.out.println("Test 2: Bad HRR exchange using secp256r1");
|
||||||
|
hrrKeyShareTest(NG_SECP256R1, false);
|
||||||
|
System.out.println();
|
||||||
|
|
||||||
|
System.out.println("Test 3: Bad HRR using unknown GC512A");
|
||||||
|
hrrKeyShareTest(NG_GC512A, false);
|
||||||
|
System.out.println();
|
||||||
|
|
||||||
|
System.out.println("Test 4: Bad HRR using known / unasserted x448");
|
||||||
|
hrrKeyShareTest(NG_X448, false);
|
||||||
|
System.out.println();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void logResult(String str, SSLEngineResult result) {
|
||||||
|
HandshakeStatus hsStatus = result.getHandshakeStatus();
|
||||||
|
System.out.println(str +
|
||||||
|
result.getStatus() + "/" + hsStatus + ", " +
|
||||||
|
result.bytesConsumed() + "/" + result.bytesProduced() +
|
||||||
|
" bytes");
|
||||||
|
if (hsStatus == HandshakeStatus.FINISHED) {
|
||||||
|
System.out.println("\t...ready for application data");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If the result indicates that we have outstanding tasks to do,
|
||||||
|
* go ahead and run them in this thread.
|
||||||
|
*/
|
||||||
|
private static void runDelegatedTasks(SSLEngine engine) throws Exception {
|
||||||
|
if (engine.getHandshakeStatus() == HandshakeStatus.NEED_TASK) {
|
||||||
|
Runnable runnable;
|
||||||
|
while ((runnable = engine.getDelegatedTask()) != null) {
|
||||||
|
System.out.println(" running delegated task...");
|
||||||
|
runnable.run();
|
||||||
|
}
|
||||||
|
HandshakeStatus hsStatus = engine.getHandshakeStatus();
|
||||||
|
if (hsStatus == HandshakeStatus.NEED_TASK) {
|
||||||
|
throw new Exception(
|
||||||
|
"handshake shouldn't need additional tasks");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dump a ByteBuffer as a hexdump to stdout. The dumping routine will
|
||||||
|
* start at the current position of the buffer and run to its limit.
|
||||||
|
* After completing the dump, the position will be returned to its
|
||||||
|
* starting point.
|
||||||
|
*
|
||||||
|
* @param data the ByteBuffer to dump to stdout.
|
||||||
|
*
|
||||||
|
* @return the hexdump of the byte array.
|
||||||
|
*/
|
||||||
|
private static String dumpHexBytes(ByteBuffer data) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
if (data != null) {
|
||||||
|
int i = 0;
|
||||||
|
data.mark();
|
||||||
|
while (data.hasRemaining()) {
|
||||||
|
if (i % 16 == 0 && i != 0) {
|
||||||
|
sb.append("\n");
|
||||||
|
}
|
||||||
|
sb.append(String.format("%02X ", data.get()));
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
data.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void hrrKeyShareTest(int hrrNamedGroup, boolean expectedPass)
|
||||||
|
throws Exception {
|
||||||
|
SSLContext sslCtx = SSLContext.getDefault();
|
||||||
|
SSLEngine engine = sslCtx.createSSLEngine();
|
||||||
|
engine.setUseClientMode(true);
|
||||||
|
SSLSession session = engine.getSession();
|
||||||
|
ByteBuffer clientOut =
|
||||||
|
ByteBuffer.wrap("I'm a Client".getBytes());
|
||||||
|
ByteBuffer cTOs = ByteBuffer.allocateDirect(
|
||||||
|
session.getPacketBufferSize());
|
||||||
|
|
||||||
|
// Create and check the ClientHello message
|
||||||
|
SSLEngineResult clientResult = engine.wrap(clientOut, cTOs);
|
||||||
|
logResult("client wrap: ", clientResult);
|
||||||
|
if (clientResult.getStatus() != SSLEngineResult.Status.OK) {
|
||||||
|
throw new RuntimeException("Client wrap got status: " +
|
||||||
|
clientResult.getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
cTOs.flip();
|
||||||
|
System.out.println("----- ORIGINAL CLIENT HELLO -----\n" +
|
||||||
|
dumpHexBytes(cTOs));
|
||||||
|
ClientHello initialCh = new ClientHello(cTOs);
|
||||||
|
|
||||||
|
if (!initialCh.suppVersions.contains(TLS_PROT_VER_13)) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Missing TLSv1.3 protocol in supported_versions");
|
||||||
|
} else if (!initialCh.keyShares.containsKey(NG_X25519) ||
|
||||||
|
!initialCh.keyShares.containsKey(NG_SECP256R1)) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Missing one or more expected KeyShares");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Craft the HRR message with the passed-in named group as the
|
||||||
|
// key share named group to request.
|
||||||
|
ByteBuffer sTOc = buildHRRMessage(initialCh, hrrNamedGroup);
|
||||||
|
System.out.println("----- SERVER HELLO RETRY REQUEST -----\n" +
|
||||||
|
dumpHexBytes(sTOc));
|
||||||
|
|
||||||
|
// Unwrap the HRR and process it
|
||||||
|
clientResult = engine.unwrap(sTOc, clientOut);
|
||||||
|
logResult("client unwrap: ", clientResult);
|
||||||
|
if (clientResult.getStatus() != SSLEngineResult.Status.OK) {
|
||||||
|
throw new RuntimeException("Client wrap got status: " +
|
||||||
|
clientResult.getStatus());
|
||||||
|
}
|
||||||
|
runDelegatedTasks(engine);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Now we're expecting to reissue the ClientHello, this time
|
||||||
|
// with a secp384r1 share.
|
||||||
|
cTOs.compact();
|
||||||
|
clientResult = engine.wrap(clientOut, cTOs);
|
||||||
|
logResult("client wrap: ", clientResult);
|
||||||
|
if (clientResult.getStatus() != SSLEngineResult.Status.OK) {
|
||||||
|
throw new RuntimeException("Client wrap got status: " +
|
||||||
|
clientResult.getStatus());
|
||||||
|
}
|
||||||
|
} catch (RuntimeException | SSLException ssle) {
|
||||||
|
if (expectedPass) {
|
||||||
|
System.out.println("Caught unexpected exception");
|
||||||
|
throw ssle;
|
||||||
|
} else {
|
||||||
|
System.out.println("Caught expected exception: " + ssle);
|
||||||
|
|
||||||
|
// Try issuing another wrap call and see if we can get
|
||||||
|
// the Alert out.
|
||||||
|
clientResult = engine.wrap(clientOut, cTOs);
|
||||||
|
logResult("client wrap: ", clientResult);
|
||||||
|
if (clientResult.getStatus() != SSLEngineResult.Status.CLOSED) {
|
||||||
|
throw new RuntimeException("Client wrap got status: " +
|
||||||
|
clientResult.getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
cTOs.flip();
|
||||||
|
System.out.println("----- ALERT -----\n" + dumpHexBytes(cTOs));
|
||||||
|
Alert alert = new Alert(cTOs);
|
||||||
|
if (alert.alertType != ALERT_TYPE_FATAL ||
|
||||||
|
alert.alertDesc != ALERT_DESC_ILLEGAL_PARAM) {
|
||||||
|
throw new RuntimeException("Unexpected alert. " +
|
||||||
|
"received " + alert.alertType + " / " +
|
||||||
|
alert.alertDesc);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cTOs.flip();
|
||||||
|
System.out.println("----- REISSUED CLIENT HELLO -----\n" +
|
||||||
|
dumpHexBytes(cTOs));
|
||||||
|
ClientHello reissuedCh = new ClientHello(cTOs);
|
||||||
|
|
||||||
|
if (!reissuedCh.keyShares.containsKey(hrrNamedGroup)) {
|
||||||
|
throw new RuntimeException("Missing secp384r1 key share");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ByteBuffer buildHRRMessage(ClientHello cliHello,
|
||||||
|
int namedGroup) throws IOException {
|
||||||
|
// Create a ByteBuffer that will be large enough to handle
|
||||||
|
// the HelloRetryRequest
|
||||||
|
ByteBuffer hrrBuf = ByteBuffer.allocate(2048); // More than enough!
|
||||||
|
|
||||||
|
// Advance past the TLS record and handshake message headers. We will
|
||||||
|
// go back later and scribble in the proper lengths. The record header
|
||||||
|
// is 5 bytes long, the handshake header is 4.
|
||||||
|
hrrBuf.position(9);
|
||||||
|
hrrBuf.putShort((short)TLS_LEGACY_VER);
|
||||||
|
hrrBuf.put(HRR_RANDOM);
|
||||||
|
hrrBuf.put((byte)cliHello.sessId.length);
|
||||||
|
hrrBuf.put(cliHello.sessId);
|
||||||
|
hrrBuf.putShort(cliHello.cipherSuites.get(0).shortValue());
|
||||||
|
hrrBuf.put((byte)COMP_NONE);
|
||||||
|
|
||||||
|
// Use a separate stream for creating the extension section
|
||||||
|
ByteArrayOutputStream extBaos = new ByteArrayOutputStream();
|
||||||
|
DataOutputStream extStream = new DataOutputStream(extBaos);
|
||||||
|
|
||||||
|
// Supported version
|
||||||
|
extStream.writeShort(HELLO_EXT_SUPP_VERS);
|
||||||
|
extStream.writeShort(2);
|
||||||
|
extStream.writeShort(TLS_PROT_VER_13);
|
||||||
|
|
||||||
|
// Key share
|
||||||
|
extStream.writeShort(HELLO_EXT_KEY_SHARE);
|
||||||
|
extStream.writeShort(2);
|
||||||
|
extStream.writeShort(namedGroup);
|
||||||
|
|
||||||
|
// Now add in the extensions into the main message
|
||||||
|
hrrBuf.putShort((short)extStream.size());
|
||||||
|
hrrBuf.put(extBaos.toByteArray());
|
||||||
|
|
||||||
|
// At this point we can go back and write in the TLS record and
|
||||||
|
// handshake message headers.
|
||||||
|
hrrBuf.flip();
|
||||||
|
|
||||||
|
// Write in the TLS record header
|
||||||
|
hrrBuf.put((byte)TLS_REC_HANDSHAKE);
|
||||||
|
hrrBuf.putShort((short)TLS_LEGACY_VER);
|
||||||
|
hrrBuf.putShort((short)(hrrBuf.limit() - 5));
|
||||||
|
|
||||||
|
// Write the Handshake message header
|
||||||
|
hrrBuf.putInt((HS_MSG_SERVHELLO << 24) |
|
||||||
|
((hrrBuf.limit() - 9) & 0x00FFFFFF));
|
||||||
|
|
||||||
|
hrrBuf.rewind();
|
||||||
|
return hrrBuf;
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user