| /* | |
| * Copyright (c) 2018, 2022, 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. Oracle designates this | |
| * particular file as subject to the "Classpath" exception as provided | |
| * by Oracle in the LICENSE file that accompanied this code. | |
| * | |
| * 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. | |
| */ | |
| package sun.security.ssl; | |
| import java.io.IOException; | |
| import java.nio.ByteBuffer; | |
| import java.text.MessageFormat; | |
| import java.util.*; | |
| import sun.security.ssl.SSLHandshake.HandshakeMessage; | |
| import sun.security.util.HexDumpEncoder; | |
| /** | |
| * SSL/(D)TLS extensions in a handshake message. | |
| */ | |
| final class SSLExtensions { | |
| private final HandshakeMessage handshakeMessage; | |
| private final Map<SSLExtension, byte[]> extMap = new LinkedHashMap<>(); | |
| private int encodedLength; | |
| // Extension map for debug logging | |
| private final Map<Integer, byte[]> logMap = | |
| SSLLogger.isOn ? new LinkedHashMap<>() : null; | |
| SSLExtensions(HandshakeMessage handshakeMessage) { | |
| this.handshakeMessage = handshakeMessage; | |
| this.encodedLength = 2; // 2: the length of the extensions. | |
| } | |
| SSLExtensions(HandshakeMessage hm, | |
| ByteBuffer m, SSLExtension[] extensions) throws IOException { | |
| this.handshakeMessage = hm; | |
| if (m.remaining() < 2) { | |
| throw hm.handshakeContext.conContext.fatal( | |
| Alert.DECODE_ERROR, | |
| "Incorrect extensions: no length field"); | |
| } | |
| int len = Record.getInt16(m); | |
| if (len > m.remaining()) { | |
| throw hm.handshakeContext.conContext.fatal( | |
| Alert.DECODE_ERROR, | |
| "Insufficient extensions data"); | |
| } | |
| encodedLength = len + 2; // 2: the length of the extensions. | |
| while (len > 0) { | |
| int extId = Record.getInt16(m); | |
| int extLen = Record.getInt16(m); | |
| if (extLen > m.remaining()) { | |
| throw hm.handshakeContext.conContext.fatal( | |
| Alert.DECODE_ERROR, | |
| "Error parsing extension (" + extId + | |
| "): no sufficient data"); | |
| } | |
| boolean isSupported = true; | |
| SSLHandshake handshakeType = hm.handshakeType(); | |
| if (SSLExtension.isConsumable(extId) && | |
| SSLExtension.valueOf(handshakeType, extId) == null) { | |
| if (extId == SSLExtension.CH_SUPPORTED_GROUPS.id && | |
| handshakeType == SSLHandshake.SERVER_HELLO) { | |
| // Note: It does not comply to the specification. However, | |
| // there are servers that send the supported_groups | |
| // extension in ServerHello handshake message. | |
| // | |
| // TLS 1.3 should not send this extension. We may want to | |
| // limit the workaround for TLS 1.2 and prior version only. | |
| // However, the implementation of the limit is complicated | |
| // and inefficient, and may not worthy the maintenance. | |
| isSupported = false; | |
| if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { | |
| SSLLogger.warning( | |
| "Received buggy supported_groups extension " + | |
| "in the ServerHello handshake message"); | |
| } | |
| } else if (handshakeType == SSLHandshake.SERVER_HELLO) { | |
| throw hm.handshakeContext.conContext.fatal( | |
| Alert.UNSUPPORTED_EXTENSION, "extension (" + | |
| extId + ") should not be presented in " + | |
| handshakeType.name); | |
| } else { | |
| isSupported = false; | |
| // debug log to ignore unknown extension for handshakeType | |
| } | |
| } | |
| if (isSupported) { | |
| isSupported = false; | |
| for (SSLExtension extension : extensions) { | |
| if ((extension.id != extId) || | |
| (extension.onLoadConsumer == null)) { | |
| continue; | |
| } | |
| if (extension.handshakeType != handshakeType) { | |
| throw hm.handshakeContext.conContext.fatal( | |
| Alert.UNSUPPORTED_EXTENSION, | |
| "extension (" + extId + ") should not be " + | |
| "presented in " + handshakeType.name); | |
| } | |
| byte[] extData = new byte[extLen]; | |
| m.get(extData); | |
| extMap.put(extension, extData); | |
| if (logMap != null) { | |
| logMap.put(extId, extData); | |
| } | |
| isSupported = true; | |
| break; | |
| } | |
| } | |
| if (!isSupported) { | |
| if (logMap != null) { | |
| // cache the extension for debug logging | |
| byte[] extData = new byte[extLen]; | |
| m.get(extData); | |
| logMap.put(extId, extData); | |
| if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { | |
| SSLLogger.fine( | |
| "Ignore unknown or unsupported extension", | |
| toString(extId, extData)); | |
| } | |
| } else { | |
| // ignore the extension | |
| int pos = m.position() + extLen; | |
| m.position(pos); | |
| } | |
| } | |
| len -= extLen + 4; | |
| } | |
| } | |
| byte[] get(SSLExtension ext) { | |
| return extMap.get(ext); | |
| } | |
| /** | |
| * Consume the specified extensions. | |
| */ | |
| void consumeOnLoad(HandshakeContext context, | |
| SSLExtension[] extensions) throws IOException { | |
| for (SSLExtension extension : extensions) { | |
| if (context.negotiatedProtocol != null && | |
| !extension.isAvailable(context.negotiatedProtocol)) { | |
| if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { | |
| SSLLogger.fine( | |
| "Ignore unsupported extension: " + extension.name); | |
| } | |
| continue; | |
| } | |
| if (!extMap.containsKey(extension)) { | |
| if (extension.onLoadAbsence != null) { | |
| extension.absentOnLoad(context, handshakeMessage); | |
| } else if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { | |
| SSLLogger.fine( | |
| "Ignore unavailable extension: " + extension.name); | |
| } | |
| continue; | |
| } | |
| if (extension.onLoadConsumer == null) { | |
| if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { | |
| SSLLogger.warning( | |
| "Ignore unsupported extension: " + extension.name); | |
| } | |
| continue; | |
| } | |
| ByteBuffer m = ByteBuffer.wrap(extMap.get(extension)); | |
| extension.consumeOnLoad(context, handshakeMessage, m); | |
| if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { | |
| SSLLogger.fine("Consumed extension: " + extension.name); | |
| } | |
| } | |
| } | |
| /** | |
| * Consider impact of the specified extensions. | |
| */ | |
| void consumeOnTrade(HandshakeContext context, | |
| SSLExtension[] extensions) throws IOException { | |
| for (SSLExtension extension : extensions) { | |
| if (!extMap.containsKey(extension)) { | |
| if (extension.onTradeAbsence != null) { | |
| extension.absentOnTrade(context, handshakeMessage); | |
| } else if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { | |
| SSLLogger.fine( | |
| "Ignore unavailable extension: " + extension.name); | |
| } | |
| continue; | |
| } | |
| if (extension.onTradeConsumer == null) { | |
| if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { | |
| SSLLogger.warning( | |
| "Ignore impact of unsupported extension: " + | |
| extension.name); | |
| } | |
| continue; | |
| } | |
| extension.consumeOnTrade(context, handshakeMessage); | |
| if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { | |
| SSLLogger.fine("Populated with extension: " + extension.name); | |
| } | |
| } | |
| } | |
| /** | |
| * Produce extension values for the specified extensions. | |
| */ | |
| void produce(HandshakeContext context, | |
| SSLExtension[] extensions) throws IOException { | |
| for (SSLExtension extension : extensions) { | |
| if (extMap.containsKey(extension)) { | |
| if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { | |
| SSLLogger.fine( | |
| "Ignore, duplicated extension: " + | |
| extension.name); | |
| } | |
| continue; | |
| } | |
| if (extension.networkProducer == null) { | |
| if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { | |
| SSLLogger.warning( | |
| "Ignore, no extension producer defined: " + | |
| extension.name); | |
| } | |
| continue; | |
| } | |
| byte[] encoded = extension.produce(context, handshakeMessage); | |
| if (encoded != null) { | |
| extMap.put(extension, encoded); | |
| encodedLength += encoded.length + 4; // extension_type (2) | |
| // extension_data length(2) | |
| } else if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { | |
| // The extension is not available in the context. | |
| SSLLogger.fine( | |
| "Ignore, context unavailable extension: " + | |
| extension.name); | |
| } | |
| } | |
| } | |
| /** | |
| * Produce extension values for the specified extensions, replacing if | |
| * there is an existing extension value for a specified extension. | |
| */ | |
| void reproduce(HandshakeContext context, | |
| SSLExtension[] extensions) throws IOException { | |
| for (SSLExtension extension : extensions) { | |
| if (extension.networkProducer == null) { | |
| if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { | |
| SSLLogger.warning( | |
| "Ignore, no extension producer defined: " + | |
| extension.name); | |
| } | |
| continue; | |
| } | |
| byte[] encoded = extension.produce(context, handshakeMessage); | |
| if (encoded != null) { | |
| if (extMap.containsKey(extension)) { | |
| byte[] old = extMap.replace(extension, encoded); | |
| if (old != null) { | |
| encodedLength -= old.length + 4; | |
| } | |
| } else { | |
| extMap.put(extension, encoded); | |
| } | |
| encodedLength += encoded.length + 4; | |
| // extension_type (2) | |
| // extension_data length(2) | |
| } else if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { | |
| // The extension is not available in the context. | |
| SSLLogger.fine( | |
| "Ignore, context unavailable extension: " + | |
| extension.name); | |
| } | |
| } | |
| } | |
| // Note that TLS 1.3 may use empty extensions. Please consider it while | |
| // using this method. | |
| int length() { | |
| if (extMap.isEmpty()) { | |
| return 0; | |
| } else { | |
| return encodedLength; | |
| } | |
| } | |
| // Note that TLS 1.3 may use empty extensions. Please consider it while | |
| // using this method. | |
| void send(HandshakeOutStream hos) throws IOException { | |
| int extsLen = length(); | |
| if (extsLen == 0) { | |
| return; | |
| } | |
| hos.putInt16(extsLen - 2); | |
| // extensions must be sent in the order they appear in the enum | |
| for (SSLExtension ext : SSLExtension.values()) { | |
| byte[] extData = extMap.get(ext); | |
| if (extData != null) { | |
| hos.putInt16(ext.id); | |
| hos.putBytes16(extData); | |
| } | |
| } | |
| } | |
| @Override | |
| public String toString() { | |
| if (extMap.isEmpty() && (logMap == null || logMap.isEmpty())) { | |
| return "<no extension>"; | |
| } else { | |
| StringBuilder builder = new StringBuilder(512); | |
| if (logMap != null && !logMap.isEmpty()) { | |
| for (Map.Entry<Integer, byte[]> en : logMap.entrySet()) { | |
| SSLExtension ext = SSLExtension.valueOf( | |
| handshakeMessage.handshakeType(), en.getKey()); | |
| if (builder.length() != 0) { | |
| builder.append(",\n"); | |
| } | |
| if (ext != null) { | |
| builder.append( | |
| ext.toString(handshakeMessage.handshakeContext, | |
| ByteBuffer.wrap(en.getValue()))); | |
| } else { | |
| builder.append(toString(en.getKey(), en.getValue())); | |
| } | |
| } | |
| } else { | |
| for (Map.Entry<SSLExtension, byte[]> en : extMap.entrySet()) { | |
| if (builder.length() != 0) { | |
| builder.append(",\n"); | |
| } | |
| builder.append( | |
| en.getKey().toString(handshakeMessage.handshakeContext, | |
| ByteBuffer.wrap(en.getValue()))); | |
| } | |
| } | |
| return builder.toString(); | |
| } | |
| } | |
| private static String toString(int extId, byte[] extData) { | |
| String extName = SSLExtension.nameOf(extId); | |
| MessageFormat messageFormat = new MessageFormat( | |
| """ | |
| "{0} ({1})": '{' | |
| {2} | |
| '}'""", | |
| Locale.ENGLISH); | |
| HexDumpEncoder hexEncoder = new HexDumpEncoder(); | |
| String encoded = hexEncoder.encodeBuffer(extData); | |
| Object[] messageFields = { | |
| extName, | |
| extId, | |
| Utilities.indent(encoded) | |
| }; | |
| return messageFormat.format(messageFields); | |
| } | |
| } |