File: TLSClientHelloExtractor.java

package info (click to toggle)
tomcat9 9.0.70-2
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 43,208 kB
  • sloc: java: 358,255; xml: 63,839; jsp: 4,528; sh: 1,204; perl: 315; makefile: 18
file content (444 lines) | stat: -rw-r--r-- 15,712 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package org.apache.tomcat.util.net;

import java.io.IOException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.buf.HexUtils;
import org.apache.tomcat.util.http.parser.HttpParser;
import org.apache.tomcat.util.net.openssl.ciphers.Cipher;
import org.apache.tomcat.util.res.StringManager;

/**
 * This class extracts the SNI host name and ALPN protocols from a TLS
 * client-hello message.
 */
public class TLSClientHelloExtractor {

    private static final Log log = LogFactory.getLog(TLSClientHelloExtractor.class);
    private static final StringManager sm = StringManager.getManager(TLSClientHelloExtractor.class);

    private final ExtractorResult result;
    private final List<Cipher> clientRequestedCiphers;
    private final List<String> clientRequestedCipherNames;
    private final String sniValue;
    private final List<String> clientRequestedApplicationProtocols;
    private final List<String> clientRequestedProtocols;

    private static final int TLS_RECORD_HEADER_LEN = 5;

    private static final int TLS_EXTENSION_SERVER_NAME = 0;
    private static final int TLS_EXTENSION_ALPN = 16;
    private static final int TLS_EXTENSION_SUPPORTED_VERSION = 43;

    public static byte[] USE_TLS_RESPONSE = ("HTTP/1.1 400 \r\n" +
            "Content-Type: text/plain;charset=UTF-8\r\n" +
            "Connection: close\r\n" +
            "\r\n" +
            "Bad Request\r\n" +
            "This combination of host and port requires TLS.\r\n").getBytes(StandardCharsets.UTF_8);


    /**
     * Creates the instance of the parser and processes the provided buffer. The
     * buffer position and limit will be modified during the execution of this
     * method but they will be returned to the original values before the method
     * exits.
     *
     * @param netInBuffer The buffer containing the TLS data to process
     * @throws IOException If the client hello message is malformed
     */
    public TLSClientHelloExtractor(ByteBuffer netInBuffer) throws IOException {
        // Buffer is in write mode at this point. Record the current position so
        // the buffer state can be restored at the end of this method.
        int pos = netInBuffer.position();
        int limit = netInBuffer.limit();
        ExtractorResult result = ExtractorResult.NOT_PRESENT;
        List<Cipher> clientRequestedCiphers = new ArrayList<>();
        List<String> clientRequestedCipherNames = new ArrayList<>();
        List<String> clientRequestedApplicationProtocols = new ArrayList<>();
        List<String> clientRequestedProtocols = new ArrayList<>();
        String sniValue = null;
        try {
            // Switch to read mode.
            netInBuffer.flip();

            // A complete TLS record header is required before we can figure out
            // how many bytes there are in the record.
            if (!isAvailable(netInBuffer, TLS_RECORD_HEADER_LEN)) {
                result = handleIncompleteRead(netInBuffer);
                return;
            }

            if (!isTLSHandshake(netInBuffer)) {
                // Is the client trying to use clear text HTTP?
                if (isHttp(netInBuffer)) {
                    result = ExtractorResult.NON_SECURE;
                }
                return;
            }

            if (!isAllRecordAvailable(netInBuffer)) {
                result = handleIncompleteRead(netInBuffer);
                return;
            }

            if (!isClientHello(netInBuffer)) {
                return;
            }

            if (!isAllClientHelloAvailable(netInBuffer)) {
                // Client hello didn't fit into single TLS record.
                // Treat this as not present.
                log.warn(sm.getString("sniExtractor.clientHelloTooBig"));
                return;
            }

            // Protocol Version
            String legacyVersion = readProtocol(netInBuffer);
            // Random
            skipBytes(netInBuffer, 32);
            // Session ID (single byte for length)
            skipBytes(netInBuffer, (netInBuffer.get() & 0xFF));

            // Cipher Suites
            // (2 bytes for length, each cipher ID is 2 bytes)
            int cipherCount = netInBuffer.getChar() / 2;
            for (int i = 0; i < cipherCount; i++) {
                char cipherId = netInBuffer.getChar();
                Cipher c = Cipher.valueOf(cipherId);
                // Some clients transmit grease values (see RFC 8701)
                if (c == null) {
                    clientRequestedCipherNames.add("Unknown(0x" + HexUtils.toHexString(cipherId) + ")");
                } else {
                    clientRequestedCiphers.add(c);
                    clientRequestedCipherNames.add(c.name());
                }
            }

            // Compression methods (single byte for length)
            skipBytes(netInBuffer, (netInBuffer.get() & 0xFF));

            if (!netInBuffer.hasRemaining()) {
                // No more data means no extensions present
                return;
            }

            // Extension length
            skipBytes(netInBuffer, 2);
            // Read the extensions until we run out of data or find the data
            // we need
            while (netInBuffer.hasRemaining() && (sniValue == null ||
                    clientRequestedApplicationProtocols.isEmpty() || clientRequestedProtocols.isEmpty())) {
                // Extension type is two byte
                char extensionType = netInBuffer.getChar();
                // Extension size is another two bytes
                char extensionDataSize = netInBuffer.getChar();
                switch (extensionType) {
                case TLS_EXTENSION_SERVER_NAME: {
                    sniValue = readSniExtension(netInBuffer);
                    break;
                }
                case TLS_EXTENSION_ALPN:
                    readAlpnExtension(netInBuffer, clientRequestedApplicationProtocols);
                    break;
                case TLS_EXTENSION_SUPPORTED_VERSION:
                    readSupportedVersions(netInBuffer, clientRequestedProtocols);
                    break;
                default: {
                    skipBytes(netInBuffer, extensionDataSize);
                }
                }
            }
            if (clientRequestedProtocols.isEmpty()) {
                clientRequestedProtocols.add(legacyVersion);
            }
            result = ExtractorResult.COMPLETE;
        } catch (BufferUnderflowException | IllegalArgumentException e) {
            throw new IOException(sm.getString("sniExtractor.clientHelloInvalid"), e);
        } finally {
            this.result = result;
            this.clientRequestedCiphers = clientRequestedCiphers;
            this.clientRequestedCipherNames = clientRequestedCipherNames;
            this.clientRequestedApplicationProtocols = clientRequestedApplicationProtocols;
            this.sniValue = sniValue;
            this.clientRequestedProtocols = clientRequestedProtocols;
            // Whatever happens, return the buffer to its original state
            netInBuffer.limit(limit);
            netInBuffer.position(pos);
        }
    }


    public ExtractorResult getResult() {
        return result;
    }


    /**
     * @return The SNI value provided by the client converted to lower case if
     *         not already lower case.
     */
    public String getSNIValue() {
        if (result == ExtractorResult.COMPLETE) {
            return sniValue;
        } else {
            throw new IllegalStateException(sm.getString("sniExtractor.tooEarly"));
        }
    }


    public List<Cipher> getClientRequestedCiphers() {
        if (result == ExtractorResult.COMPLETE || result == ExtractorResult.NOT_PRESENT) {
            return clientRequestedCiphers;
        } else {
            throw new IllegalStateException(sm.getString("sniExtractor.tooEarly"));
        }
    }


    public List<String> getClientRequestedCipherNames() {
        if (result == ExtractorResult.COMPLETE || result == ExtractorResult.NOT_PRESENT) {
            return clientRequestedCipherNames;
        } else {
            throw new IllegalStateException(sm.getString("sniExtractor.tooEarly"));
        }
    }


    public List<String> getClientRequestedApplicationProtocols() {
        if (result == ExtractorResult.COMPLETE || result == ExtractorResult.NOT_PRESENT) {
            return clientRequestedApplicationProtocols;
        } else {
            throw new IllegalStateException(sm.getString("sniExtractor.tooEarly"));
        }
    }


    public List<String> getClientRequestedProtocols() {
        if (result == ExtractorResult.COMPLETE || result == ExtractorResult.NOT_PRESENT) {
            return clientRequestedProtocols;
        } else {
            throw new IllegalStateException(sm.getString("sniExtractor.tooEarly"));
        }
    }


    private static ExtractorResult handleIncompleteRead(ByteBuffer bb) {
        if (bb.limit() == bb.capacity()) {
            // Buffer not big enough
            return ExtractorResult.UNDERFLOW;
        } else {
            // Need to read more data into buffer
            return ExtractorResult.NEED_READ;
        }
    }


    private static boolean isAvailable(ByteBuffer bb, int size) {
        if (bb.remaining() < size) {
            bb.position(bb.limit());
            return false;
        }
        return true;
    }


    private static boolean isTLSHandshake(ByteBuffer bb) {
        // For a TLS client hello the first byte must be 22 - handshake
        if (bb.get() != 22) {
            return false;
        }
        // Next two bytes are major/minor version. We need at least 3.1.
        byte b2 = bb.get();
        byte b3 = bb.get();
        if (b2 < 3 || b2 == 3 && b3 == 0) {
            return false;
        }
        return true;
    }


    private static boolean isHttp(ByteBuffer bb) {
        // Based on code in Http11InputBuffer
        // Note: The actual request is not important. This code only checks that
        //       the buffer contains a correctly formatted HTTP request line.
        //       The method, target and protocol are not validated.
        byte chr = 0;
        bb.position(0);

        // Skip blank lines
        do {
            if (!bb.hasRemaining()) {
                return false;
            }
            chr = bb.get();
        } while (chr == '\r' || chr == '\n');

        // Read the method
        do {
            if (!HttpParser.isToken(chr) || !bb.hasRemaining()) {
                return false;
            }
            chr = bb.get();
        } while (chr != ' ' && chr != '\t');

        // Whitespace between method and target
        while (chr == ' ' || chr == '\t') {
            if (!bb.hasRemaining()) {
                return false;
            }
            chr = bb.get();
        }

        // Read the target
        while (chr != ' ' && chr != '\t') {
            if (HttpParser.isNotRequestTarget(chr) || !bb.hasRemaining()) {
                return false;
            }
            chr = bb.get();
        }

        // Whitespace between target and protocol
        while (chr == ' ' || chr == '\t') {
            if (!bb.hasRemaining()) {
                return false;
            }
            chr = bb.get();
        }

        // Read protocol
        do {
            if (!HttpParser.isHttpProtocol(chr) || !bb.hasRemaining()) {
                return false;
            }
            chr = bb.get();

        } while (chr != '\r' && chr != '\n');

        return true;
    }


    private static boolean isAllRecordAvailable(ByteBuffer bb) {
        // Next two bytes (unsigned) are the size of the record. We need all of
        // it.
        int size = bb.getChar();
        return isAvailable(bb, size);
    }


    private static boolean isClientHello(ByteBuffer bb) {
        // Client hello is handshake type 1
        if (bb.get() == 1) {
            return true;
        }
        return false;
    }


    private static boolean isAllClientHelloAvailable(ByteBuffer bb) {
        // Next three bytes (unsigned) are the size of the client hello. We need
        // all of it.
        int size = ((bb.get() & 0xFF) << 16) + ((bb.get() & 0xFF) << 8) + (bb.get() & 0xFF);
        return isAvailable(bb, size);
    }


    private static void skipBytes(ByteBuffer bb, int size) {
        bb.position(bb.position() + size);
    }


    private static String readProtocol(ByteBuffer bb) {
        char protocol = bb.getChar();
        switch (protocol) {
            case 0x0300: {
                return Constants.SSL_PROTO_SSLv3;
            }
            case 0x0301: {
                return Constants.SSL_PROTO_TLSv1_0;
            }
            case 0x0302: {
                return Constants.SSL_PROTO_TLSv1_1;
            }
            case 0x0303: {
                return Constants.SSL_PROTO_TLSv1_2;
            }
            case 0x0304: {
                return Constants.SSL_PROTO_TLSv1_3;
            }
            default:
                return "Unknown(0x" + HexUtils.toHexString(protocol) + ")";
        }
    }


    private static String readSniExtension(ByteBuffer bb) {
        // First 2 bytes are size of server name list (only expecting one)
        // Next byte is type (0 for hostname)
        skipBytes(bb, 3);
        // Next 2 bytes are length of host name
        char serverNameSize = bb.getChar();
        byte[] serverNameBytes = new byte[serverNameSize];
        bb.get(serverNameBytes);
        return new String(serverNameBytes, StandardCharsets.UTF_8).toLowerCase(Locale.ENGLISH);
    }


    private static void readAlpnExtension(ByteBuffer bb, List<String> protocolNames) {
        // First 2 bytes are size of the protocol list
        char toRead = bb.getChar();
        byte[] inputBuffer = new byte[255];
        while (toRead > 0) {
            // Each list entry has one byte for length followed by a string of
            // that length
            int len = bb.get() & 0xFF;
            bb.get(inputBuffer, 0, len);
            protocolNames.add(new String(inputBuffer, 0, len, StandardCharsets.UTF_8));
            toRead--;
            toRead -= len;
        }
    }


    private static void readSupportedVersions(ByteBuffer bb, List<String> protocolNames) {
        // First byte is the size of the list in bytes
        int count = (bb.get() & 0xFF) / 2;
        // Then the list of protocols
        for (int i = 0; i < count; i++) {
            protocolNames.add(readProtocol(bb));
        }
    }


    public enum ExtractorResult {
        COMPLETE,
        NOT_PRESENT,
        UNDERFLOW,
        NEED_READ,
        NON_SECURE
    }
}