1 /*
2  * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.
8  *
9  * This code is distributed in the hope that it will be useful, but WITHOUT
10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12  * version 2 for more details (a copy is included in the LICENSE file that
13  * accompanied this code).
14  *
15  * You should have received a copy of the GNU General Public License version
16  * 2 along with this work; if not, write to the Free Software Foundation,
17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18  *
19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20  * or visit www.oracle.com if you need additional information or have any
21  * questions.
22  */
23 
24 // SunJSSE does not support dynamic system properties, no way to re-use
25 // system properties in samevm/agentvm mode.  For further debugging output
26 // set the -Djavax.net.debug=ssl:handshake property on the @run lines.
27 
28 /*
29  * @test
30  * @bug 8247630
31  * @summary Use two key share entries
32  * @run main/othervm ClientHelloKeyShares 29 23
33  * @run main/othervm -Djdk.tls.namedGroups=secp384r1,secp521r1,x448,ffdhe2048 ClientHelloKeyShares 24 30
34  * @run main/othervm -Djdk.tls.namedGroups=sect163k1,sect163r1,x25519 ClientHelloKeyShares 29
35  * @run main/othervm -Djdk.tls.namedGroups=sect163k1,sect163r1,secp256r1 ClientHelloKeyShares 23
36  * @run main/othervm -Djdk.tls.namedGroups=sect163k1,sect163r1,ffdhe2048,ffdhe3072,ffdhe4096 ClientHelloKeyShares 256
37  * @run main/othervm -Djdk.tls.namedGroups=sect163k1,ffdhe2048,x25519,secp256r1 ClientHelloKeyShares 256 29
38  * @run main/othervm -Djdk.tls.namedGroups=secp256r1,secp384r1,ffdhe2048,x25519 ClientHelloKeyShares 23 256
39  */
40 
41 import javax.net.ssl.*;
42 import javax.net.ssl.SSLEngineResult.*;
43 import java.nio.ByteBuffer;
44 import java.util.*;
45 
46 
47 public class ClientHelloKeyShares {
48 
49     // Some TLS constants we'll use for testing
50     private static final int TLS_REC_HANDSHAKE = 22;
51     private static final int HELLO_EXT_SUPP_GROUPS = 10;
52     private static final int HELLO_EXT_SUPP_VERS = 43;
53     private static final int HELLO_EXT_KEY_SHARE = 51;
54     private static final int TLS_PROT_VER_13 = 0x0304;
55     private static final int NG_SECP256R1 = 0x0017;
56     private static final int NG_SECP384R1 = 0x0018;
57     private static final int NG_X25519 = 0x001D;
58     private static final int NG_X448 = 0x001E;
59 
main(String args[])60     public static void main(String args[]) throws Exception {
61         // Arguments to this test are an abitrary number of integer
62         // values which will be the expected NamedGroup IDs in the key_share
63         // extension.  Expected named group assertions may also be affected
64         // by setting the jdk.tls.namedGroups System property.
65         List<Integer> expectedKeyShares = new ArrayList<>();
66         Arrays.stream(args).forEach(arg ->
67                 expectedKeyShares.add(Integer.valueOf(arg)));
68 
69         SSLContext sslCtx = SSLContext.getDefault();
70         SSLEngine engine = sslCtx.createSSLEngine();
71         engine.setUseClientMode(true);
72         SSLSession session = engine.getSession();
73         ByteBuffer clientOut = ByteBuffer.wrap("I'm a Client".getBytes());
74         ByteBuffer cTOs =
75                 ByteBuffer.allocateDirect(session.getPacketBufferSize());
76 
77         // Create and check the ClientHello message
78         SSLEngineResult clientResult = engine.wrap(clientOut, cTOs);
79         logResult("client wrap: ", clientResult);
80         if (clientResult.getStatus() != SSLEngineResult.Status.OK) {
81             throw new RuntimeException("Client wrap got status: " +
82                     clientResult.getStatus());
83         }
84 
85         cTOs.flip();
86         System.out.println(dumpHexBytes(cTOs));
87         checkClientHello(cTOs, expectedKeyShares);
88     }
89 
logResult(String str, SSLEngineResult result)90     private static void logResult(String str, SSLEngineResult result) {
91         HandshakeStatus hsStatus = result.getHandshakeStatus();
92         System.out.println(str +
93             result.getStatus() + "/" + hsStatus + ", " +
94             result.bytesConsumed() + "/" + result.bytesProduced() +
95             " bytes");
96         if (hsStatus == HandshakeStatus.FINISHED) {
97             System.out.println("\t...ready for application data");
98         }
99     }
100 
101     /**
102      * Dump a ByteBuffer as a hexdump to stdout.  The dumping routine will
103      * start at the current position of the buffer and run to its limit.
104      * After completing the dump, the position will be returned to its
105      * starting point.
106      *
107      * @param data the ByteBuffer to dump to stdout.
108      *
109      * @return the hexdump of the byte array.
110      */
dumpHexBytes(ByteBuffer data)111     private static String dumpHexBytes(ByteBuffer data) {
112         StringBuilder sb = new StringBuilder();
113         if (data != null) {
114             int i = 0;
115             data.mark();
116             while (data.hasRemaining()) {
117                 if (i % 16 == 0 && i != 0) {
118                     sb.append("\n");
119                 }
120                 sb.append(String.format("%02X ", data.get()));
121                 i++;
122             }
123             data.reset();
124         }
125 
126         return sb.toString();
127     }
128 
129     /**
130      * Tests the ClientHello for the presence of the key shares in the supplied
131      * List of key share identifiers.
132      *
133      * @param data the ByteBuffer containing the ClientHello bytes
134      * @param keyShareTypes a List containing the expected key shares
135      *
136      * @throws RuntimeException if there is a deviation between what is expected
137      * and what is supplied.  It will also throw this exception if other
138      * basic structural elements of the ClientHello are not found (e.g. TLS 1.3
139      * is not in the list of supported groups, etc.)
140      */
checkClientHello(ByteBuffer data, List<Integer> expectedKeyShares)141     private static void checkClientHello(ByteBuffer data,
142             List<Integer> expectedKeyShares) {
143         Objects.requireNonNull(data);
144         data.mark();
145 
146         // Process the TLS record header
147         int type = Byte.toUnsignedInt(data.get());
148         int ver_major = Byte.toUnsignedInt(data.get());
149         int ver_minor = Byte.toUnsignedInt(data.get());
150         int recLen = Short.toUnsignedInt(data.getShort());
151 
152         // Simple sanity checks
153         if (type != 22) {
154             throw new RuntimeException("Not a handshake: Type = " + type);
155         } else if (recLen > data.remaining()) {
156             throw new RuntimeException("Incomplete record in buffer: " +
157                     "Record length = " + recLen + ", Remaining = " +
158                     data.remaining());
159         }
160 
161         // Grab the handshake message header.
162         int msgHdr = data.getInt();
163         int msgType = (msgHdr >> 24) & 0x000000FF;
164         int msgLen = msgHdr & 0x00FFFFFF;
165 
166         // More simple sanity checks
167         if (msgType != 1) {
168             throw new RuntimeException("Not a ClientHello: Type = " + msgType);
169         }
170 
171         // Skip over the protocol version and client random
172         data.position(data.position() + 34);
173 
174         // Jump past the session ID (if there is one)
175         int sessLen = Byte.toUnsignedInt(data.get());
176         if (sessLen != 0) {
177             data.position(data.position() + sessLen);
178         }
179 
180         // Jump past the cipher suites
181         int csLen = Short.toUnsignedInt(data.getShort());
182         if (csLen != 0) {
183             data.position(data.position() + csLen);
184         }
185 
186         // ...and the compression
187         int compLen = Byte.toUnsignedInt(data.get());
188         if (compLen != 0) {
189             data.position(data.position() + compLen);
190         }
191 
192         // Now for the fun part.  Go through the extensions and look
193         // for supported_versions (to make sure TLS 1.3 is asserted) and
194         // the expected key shares are present.
195         boolean foundSupVer = false;
196         boolean foundKeyShare = false;
197         int extsLen = Short.toUnsignedInt(data.getShort());
198         List<Integer> supGrpList = new ArrayList<>();
199         List<Integer> chKeyShares = new ArrayList<>();
200         while (data.hasRemaining()) {
201             int extType = Short.toUnsignedInt(data.getShort());
202             int extLen = Short.toUnsignedInt(data.getShort());
203             boolean foundTLS13 = false;
204             switch (extType) {
205                 case HELLO_EXT_SUPP_GROUPS:
206                     int supGrpLen = Short.toUnsignedInt(data.getShort());
207                     for (int remain = supGrpLen; remain > 0; remain -= 2) {
208                         supGrpList.add(Short.toUnsignedInt(data.getShort()));
209                     }
210                     break;
211                 case HELLO_EXT_SUPP_VERS:
212                     foundSupVer = true;
213                     int supVerLen = Byte.toUnsignedInt(data.get());
214                     for (int remain = supVerLen; remain > 0; remain -= 2) {
215                         foundTLS13 |= (Short.toUnsignedInt(data.getShort()) ==
216                                 TLS_PROT_VER_13);
217                     }
218 
219                     if (!foundTLS13) {
220                         throw new RuntimeException("Missing TLS 1.3 Protocol " +
221                                 "Version in supported_groups");
222                     }
223                     break;
224                 case HELLO_EXT_KEY_SHARE:
225                     foundKeyShare = true;
226                     int ksListLen = Short.toUnsignedInt(data.getShort());
227                     while (ksListLen > 0) {
228                         chKeyShares.add(Short.toUnsignedInt(data.getShort()));
229                         int ksLen = Short.toUnsignedInt(data.getShort());
230                         data.position(data.position() + ksLen);
231                         ksListLen -= (4 + ksLen);
232                     }
233                     break;
234                 default:
235                     data.position(data.position() + extLen);
236                     break;
237             }
238         }
239 
240         // We must have parsed supported_versions, key_share and
241         // supported_groups extensions.
242         if ((foundSupVer && foundKeyShare && !supGrpList.isEmpty()) == false) {
243             throw new RuntimeException("Missing one or more of key_share, " +
244                     "supported_versions and/or supported_groups extensions");
245         }
246 
247         // The key share types we expected in the test should match exactly what
248         // was asserted in the client hello
249         if (!expectedKeyShares.equals(chKeyShares)) {
250             StringBuilder sb = new StringBuilder(
251                     "Expected and Actual key_share lists differ: ");
252             sb.append("Expected: ");
253             expectedKeyShares.forEach(ng -> sb.append(ng).append(" "));
254             sb.append(", Actual: ");
255             chKeyShares.forEach(ng -> sb.append(ng).append(" "));
256             throw new RuntimeException(sb.toString());
257         }
258 
259         // The order of the key shares should match the order of precedence
260         // of the same named groups asserted in the supported_groups extension.
261         // (RFC 8446, 4.2.8)
262         int prevChNg = -1;
263         for (int ng : chKeyShares) {
264             int chNgPos = supGrpList.indexOf(ng);
265             if (chNgPos <= prevChNg) {
266                 throw new RuntimeException("Order of precedence violation " +
267                         "for NamedGroup " + ng + " between key_share and " +
268                         "supported_groups extensions");
269             }
270             prevChNg = chNgPos;
271         }
272 
273         // We should be at the end of the ClientHello
274         data.reset();
275     }
276 }
277