< prev index next >

src/java.base/share/classes/sun/security/ssl/AlpnExtension.java

Print this page

        

@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2018, 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

@@ -24,145 +24,473 @@
  */
 
 package sun.security.ssl;
 
 import java.io.IOException;
-import java.nio.charset.*;
-import java.util.*;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLProtocolException;
+import javax.net.ssl.SSLSocket;
+import sun.security.ssl.SSLExtension.ExtensionConsumer;
+import sun.security.ssl.SSLExtension.SSLExtensionSpec;
+import sun.security.ssl.SSLHandshake.HandshakeMessage;
 
-import javax.net.ssl.*;
+/**
+ * Pack of the "application_layer_protocol_negotiation" extensions [RFC 7301].
+ */
+final class AlpnExtension {
+    static final HandshakeProducer chNetworkProducer = new CHAlpnProducer();
+    static final ExtensionConsumer chOnLoadConcumer = new CHAlpnConsumer();
+    static final HandshakeAbsence chOnLoadAbsence = new CHAlpnAbsence();
+
+    static final HandshakeProducer shNetworkProducer = new SHAlpnProducer();
+    static final ExtensionConsumer shOnLoadConcumer = new SHAlpnConsumer();
+    static final HandshakeAbsence shOnLoadAbsence = new SHAlpnAbsence();
+
+    // Note: we reuse ServerHello operations for EncryptedExtensions for now.
+    // Please be careful about any code or specification changes in the future.
+    static final HandshakeProducer eeNetworkProducer = new SHAlpnProducer();
+    static final ExtensionConsumer eeOnLoadConcumer = new SHAlpnConsumer();
+    static final HandshakeAbsence eeOnLoadAbsence = new SHAlpnAbsence();
 
-/*
- * [RFC 7301]
- * This TLS extension facilitates the negotiation of application-layer protocols
- * within the TLS handshake. Clients MAY include an extension of type
- * "application_layer_protocol_negotiation" in the (extended) ClientHello
- * message. The "extension_data" field of this extension SHALL contain a
- * "ProtocolNameList" value:
- *
- *     enum {
- *         application_layer_protocol_negotiation(16), (65535)
- *     } ExtensionType;
- *
- *     opaque ProtocolName<1..2^8-1>;
+    static final SSLStringize alpnStringize = new AlpnStringize();
+
+    /**
+     * The "application_layer_protocol_negotiation" extension.
  *
- *     struct {
- *         ProtocolName protocol_name_list<2..2^16-1>
- *     } ProtocolNameList;
- */
-final class ALPNExtension extends HelloExtension {
-
-    final static int ALPN_HEADER_LENGTH = 1;
-    final static int MAX_APPLICATION_PROTOCOL_LENGTH = 255;
-    final static int MAX_APPLICATION_PROTOCOL_LIST_LENGTH = 65535;
-    private int listLength = 0;     // ProtocolNameList length
-    private List<String> protocolNames = null;
-
-    // constructor for ServerHello
-    ALPNExtension(String protocolName) throws SSLException {
-        this(new String[]{ protocolName });
-    }
-
-    // constructor for ClientHello
-    ALPNExtension(String[] protocolNames) throws SSLException {
-        super(ExtensionType.EXT_ALPN);
-        if (protocolNames.length == 0) { // never null, never empty
-            throw new IllegalArgumentException(
-                "The list of application protocols cannot be empty");
-        }
-        this.protocolNames = Arrays.asList(protocolNames);
-        for (String p : protocolNames) {
-            int length = p.getBytes(StandardCharsets.UTF_8).length;
-            if (length == 0) {
+     * See RFC 7301 for the specification of this extension.
+     */
+    static final class AlpnSpec implements SSLExtensionSpec {
+        final List<String> applicationProtocols;
+
+        private AlpnSpec(String[] applicationProtocols) {
+            this.applicationProtocols = Collections.unmodifiableList(
+                    Arrays.asList(applicationProtocols));
+        }
+
+        private AlpnSpec(ByteBuffer buffer) throws IOException {
+            // ProtocolName protocol_name_list<2..2^16-1>, RFC 7301.
+            if (buffer.remaining() < 2) {
                 throw new SSLProtocolException(
-                    "Application protocol name is empty");
+                    "Invalid application_layer_protocol_negotiation: " +
+                    "insufficient data (length=" + buffer.remaining() + ")");
             }
-            if (length <= MAX_APPLICATION_PROTOCOL_LENGTH) {
-                listLength += length + ALPN_HEADER_LENGTH;
-            } else {
+
+            int listLen = Record.getInt16(buffer);
+            if (listLen < 2 || listLen != buffer.remaining()) {
                 throw new SSLProtocolException(
-                    "Application protocol name is too long: " + p);
+                    "Invalid application_layer_protocol_negotiation: " +
+                    "incorrect list length (length=" + listLen + ")");
             }
-            if (listLength > MAX_APPLICATION_PROTOCOL_LIST_LENGTH) {
+
+            List<String> protocolNames = new LinkedList<>();
+            while (buffer.hasRemaining()) {
+                // opaque ProtocolName<1..2^8-1>, RFC 7301.
+                byte[] bytes = Record.getBytes8(buffer);
+                if (bytes.length == 0) {
                 throw new SSLProtocolException(
-                    "Application protocol name list is too long");
+                        "Invalid application_layer_protocol_negotiation " +
+                        "extension: empty application protocol name");
             }
+
+                String appProtocol = new String(bytes, StandardCharsets.UTF_8);
+                protocolNames.add(appProtocol);
         }
+
+            this.applicationProtocols =
+                    Collections.unmodifiableList(protocolNames);
     }
 
-    // constructor for ServerHello for parsing ALPN extension
-    ALPNExtension(HandshakeInStream s, int len) throws IOException {
-        super(ExtensionType.EXT_ALPN);
+        @Override
+        public String toString() {
+            return applicationProtocols.toString();
+        }
+    }
 
-        if (len >= 2) {
-            listLength = s.getInt16(); // list length
-            if (listLength < 2 || listLength + 2 != len) {
-                throw new SSLProtocolException(
-                    "Invalid " + type + " extension: incorrect list length " +
-                    "(length=" + listLength + ")");
+    private static final class AlpnStringize implements SSLStringize {
+        @Override
+        public String toString(ByteBuffer buffer) {
+            try {
+                return (new AlpnSpec(buffer)).toString();
+            } catch (IOException ioe) {
+                // For debug logging only, so please swallow exceptions.
+                return ioe.getMessage();
+            }
             }
-        } else {
-            throw new SSLProtocolException(
-                "Invalid " + type + " extension: insufficient data " +
-                "(length=" + len + ")");
         }
 
-        int remaining = listLength;
-        this.protocolNames = new ArrayList<>();
-        while (remaining > 0) {
-            // opaque ProtocolName<1..2^8-1>; // RFC 7301
-            byte[] bytes = s.getBytes8();
-            if (bytes.length == 0) {
-                throw new SSLProtocolException("Invalid " + type +
-                    " extension: empty application protocol name");
+    /**
+     * Network data producer of the extension in a ClientHello
+     * handshake message.
+     */
+    private static final class CHAlpnProducer implements HandshakeProducer {
+        static final int MAX_AP_LENGTH = 255;
+        static final int MAX_AP_LIST_LENGTH = 65535;
+
+        // Prevent instantiation of this class.
+        private CHAlpnProducer() {
+            // blank
+        }
+
+        @Override
+        public byte[] produce(ConnectionContext context,
+                HandshakeMessage message) throws IOException {
+            // The producing happens in client side only.
+            ClientHandshakeContext chc = (ClientHandshakeContext)context;
+
+            // Is it a supported and enabled extension?
+            if (!chc.sslConfig.isAvailable(SSLExtension.CH_ALPN)) {
+                if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
+                    SSLLogger.info(
+                            "Ignore client unavailable extension: " +
+                            SSLExtension.CH_ALPN.name);
+                }
+                return null;
+            }
+
+            String[] laps = chc.sslConfig.applicationProtocols;
+            if ((laps == null) || (laps.length == 0)) {
+                if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
+                    SSLLogger.info(
+                            "No available application protocols");
+                }
+                return null;
+            }
+
+            // Produce the extension.
+            int listLength = 0;     // ProtocolNameList length
+            for (String ap : laps) {
+                int length = ap.getBytes(StandardCharsets.UTF_8).length;
+                if (length == 0) {
+                    // log the configuration problem
+                    if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
+                        SSLLogger.severe(
+                                "Application protocol name cannot be empty");
+                    }
+                    chc.conContext.fatal(Alert.ILLEGAL_PARAMETER,
+                            "Application protocol name cannot be empty");
+                }
+
+                if (length <= MAX_AP_LENGTH) {
+                    // opaque ProtocolName<1..2^8-1>, RFC 7301.
+                    listLength += (length + 1);
+                } else {
+                    // log the configuration problem
+                    if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
+                        SSLLogger.severe(
+                                "Application protocol name (" + ap +
+                                ") exceeds the size limit (" +
+                                MAX_AP_LENGTH + " bytes)");
             }
-            String p =
-                new String(bytes, StandardCharsets.UTF_8); // app protocol
-            protocolNames.add(p);
-            remaining -= bytes.length + ALPN_HEADER_LENGTH;
+                    chc.conContext.fatal(Alert.ILLEGAL_PARAMETER,
+                                "Application protocol name (" + ap +
+                                ") exceeds the size limit (" +
+                                MAX_AP_LENGTH + " bytes)");
         }
 
-        if (remaining != 0) {
-            throw new SSLProtocolException(
-                "Invalid " + type + " extension: extra data " +
-                "(length=" + remaining + ")");
+                if (listLength > MAX_AP_LIST_LENGTH) {
+                    // log the configuration problem
+                    if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
+                        SSLLogger.severe(
+                                "The configured application protocols (" +
+                                Arrays.toString(laps) +
+                                ") exceed the size limit (" +
+                                MAX_AP_LIST_LENGTH + " bytes)");
+                    }
+                    chc.conContext.fatal(Alert.ILLEGAL_PARAMETER,
+                                "The configured application protocols (" +
+                                Arrays.toString(laps) +
+                                ") exceed the size limit (" +
+                                MAX_AP_LIST_LENGTH + " bytes)");
         }
     }
 
-    List<String> getPeerAPs() {
-        return protocolNames;
+            // ProtocolName protocol_name_list<2..2^16-1>, RFC 7301.
+            byte[] extData = new byte[listLength + 2];
+            ByteBuffer m = ByteBuffer.wrap(extData);
+            Record.putInt16(m, listLength);
+            for (String ap : laps) {
+                Record.putBytes8(m, ap.getBytes(StandardCharsets.UTF_8));
     }
 
-    /*
-     * Return the length in bytes, including extension type and length fields.
+            // Update the context.
+            chc.handshakeExtensions.put(SSLExtension.CH_ALPN,
+                    new AlpnSpec(chc.sslConfig.applicationProtocols));
+
+            return extData;
+        }
+    }
+
+    /**
+     * Network data consumer of the extension in a ClientHello
+     * handshake message.
      */
+    private static final class CHAlpnConsumer implements ExtensionConsumer {
+        // Prevent instantiation of this class.
+        private CHAlpnConsumer() {
+            // blank
+        }
+
     @Override
-    int length() {
-        return 6 + listLength;
+        public void consume(ConnectionContext context,
+            HandshakeMessage message, ByteBuffer buffer) throws IOException {
+            // The comsuming happens in server side only.
+            ServerHandshakeContext shc = (ServerHandshakeContext)context;
+
+            // Is it a supported and enabled extension?
+            if (!shc.sslConfig.isAvailable(SSLExtension.CH_ALPN)) {
+                shc.applicationProtocol = "";
+                shc.conContext.applicationProtocol = "";
+                if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
+                    SSLLogger.info(
+                            "Ignore server unavailable extension: " +
+                            SSLExtension.CH_ALPN.name);
+                }
+                return;     // ignore the extension
+            }
+
+            // Is the extension enabled?
+            boolean noAPSelector;
+            if (shc.conContext.transport instanceof SSLEngine) {
+                noAPSelector = (shc.sslConfig.engineAPSelector == null);
+            } else {
+                noAPSelector = (shc.sslConfig.socketAPSelector == null);
     }
 
+            boolean noAlpnProtocols =
+                    shc.sslConfig.applicationProtocols == null ||
+                    shc.sslConfig.applicationProtocols.length == 0;
+            if (noAPSelector && noAlpnProtocols) {
+                shc.applicationProtocol = "";
+                shc.conContext.applicationProtocol = "";
+                if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
+                    SSLLogger.fine(
+                            "Ignore server unenabled extension: " +
+                            SSLExtension.CH_ALPN.name);
+                }
+                return;     // ignore the extension
+            }
+
+            // Parse the extension.
+            AlpnSpec spec;
+            try {
+                spec = new AlpnSpec(buffer);
+            } catch (IOException ioe) {
+                shc.conContext.fatal(Alert.UNEXPECTED_MESSAGE, ioe);
+                return;     // fatal() always throws, make the compiler happy.
+            }
+
+            // Update the context.
+            if (noAPSelector) {     // noAlpnProtocols is false
+                List<String> protocolNames = spec.applicationProtocols;
+                boolean matched = false;
+                // Use server application protocol preference order.
+                for (String ap : shc.sslConfig.applicationProtocols) {
+                    if (protocolNames.contains(ap)) {
+                        shc.applicationProtocol = ap;
+                        shc.conContext.applicationProtocol = ap;
+                        matched = true;
+                        break;
+                    }
+                }
+
+                if (!matched) {
+                    shc.conContext.fatal(Alert.NO_APPLICATION_PROTOCOL,
+                            "No matching application layer protocol values");
+                }
+            }   // Otherwise, applicationProtocol will be set by the
+                // application selector callback later.
+
+            shc.handshakeExtensions.put(SSLExtension.CH_ALPN, spec);
+
+            // No impact on session resumption.
+            //
+            // [RFC 7301] Unlike many other TLS extensions, this extension
+            // does not establish properties of the session, only of the
+            // connection.  When session resumption or session tickets are
+            // used, the previous contents of this extension are irrelevant,
+            // and only the values in the new handshake messages are
+            // considered.
+        }
+    }
+
+    /**
+     * The absence processing if the extension is not present in
+     * a ClientHello handshake message.
+     */
+    private static final class CHAlpnAbsence implements HandshakeAbsence {
     @Override
-    void send(HandshakeOutStream s) throws IOException {
-        s.putInt16(type.id);
-        s.putInt16(listLength + 2); // length of extension_data
-        s.putInt16(listLength);     // length of ProtocolNameList
+        public void absent(ConnectionContext context,
+                HandshakeMessage message) throws IOException {
+            // The producing happens in server side only.
+            ServerHandshakeContext shc = (ServerHandshakeContext)context;
 
-        for (String p : protocolNames) {
-            s.putBytes8(p.getBytes(StandardCharsets.UTF_8));
+            // Please don't use the previous negotiated application protocol.
+            shc.applicationProtocol = "";
+            shc.conContext.applicationProtocol = "";
         }
     }
 
+    /**
+     * Network data producer of the extension in the ServerHello
+     * handshake message.
+     */
+    private static final class SHAlpnProducer implements HandshakeProducer {
+        // Prevent instantiation of this class.
+        private SHAlpnProducer() {
+            // blank
+        }
+
     @Override
-    public String toString() {
-        StringBuilder sb = new StringBuilder();
-        if (protocolNames == null || protocolNames.isEmpty()) {
-            sb.append("<empty>");
+        public byte[] produce(ConnectionContext context,
+                HandshakeMessage message) throws IOException {
+            // The producing happens in client side only.
+            ServerHandshakeContext shc = (ServerHandshakeContext)context;
+
+            // In response to ALPN request only
+            AlpnSpec requestedAlps =
+                    (AlpnSpec)shc.handshakeExtensions.get(SSLExtension.CH_ALPN);
+            if (requestedAlps == null) {
+                // Ignore, this extension was not requested and accepted.
+                if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
+                    SSLLogger.fine(
+                            "Ignore unavailable extension: " +
+                            SSLExtension.SH_ALPN.name);
+                }
+                return null;
+            }
+
+            List<String> alps = requestedAlps.applicationProtocols;
+            if (shc.conContext.transport instanceof SSLEngine) {
+                if (shc.sslConfig.engineAPSelector != null) {
+                    SSLEngine engine = (SSLEngine)shc.conContext.transport;
+                    shc.applicationProtocol =
+                        shc.sslConfig.engineAPSelector.apply(engine, alps);
+                    if ((shc.applicationProtocol == null) ||
+                            (!shc.applicationProtocol.isEmpty() &&
+                            !alps.contains(shc.applicationProtocol))) {
+                        shc.conContext.fatal(Alert.NO_APPLICATION_PROTOCOL,
+                            "No matching application layer protocol values");
+                    }
+                }
         } else {
-            for (String protocolName : protocolNames) {
-                sb.append("[" + protocolName + "]");
+                if (shc.sslConfig.socketAPSelector != null) {
+                    SSLSocket socket = (SSLSocket)shc.conContext.transport;
+                    shc.applicationProtocol =
+                        shc.sslConfig.socketAPSelector.apply(socket, alps);
+                    if ((shc.applicationProtocol == null) ||
+                            (!shc.applicationProtocol.isEmpty() &&
+                            !alps.contains(shc.applicationProtocol))) {
+                        shc.conContext.fatal(Alert.NO_APPLICATION_PROTOCOL,
+                            "No matching application layer protocol values");
+                    }
+                }
+            }
+
+            if ((shc.applicationProtocol == null) ||
+                    (shc.applicationProtocol.isEmpty())) {
+                // Ignore, no negotiated application layer protocol.
+                shc.applicationProtocol = "";
+                shc.conContext.applicationProtocol = "";
+                if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
+                    SSLLogger.warning(
+                        "Ignore, no negotiated application layer protocol");
+                }
+
+                return null;
             }
+
+            // opaque ProtocolName<1..2^8-1>, RFC 7301.
+            int listLen = shc.applicationProtocol.length() + 1;
+                                                        // 1: length byte
+            // ProtocolName protocol_name_list<2..2^16-1>, RFC 7301.
+            byte[] extData = new byte[listLen + 2];     // 2: list length
+            ByteBuffer m = ByteBuffer.wrap(extData);
+            Record.putInt16(m, listLen);
+            Record.putBytes8(m,
+                    shc.applicationProtocol.getBytes(StandardCharsets.UTF_8));
+
+            // Update the context.
+            shc.conContext.applicationProtocol = shc.applicationProtocol;
+
+            // Clean or register the extension
+            //
+            // No further use of the request and respond extension any more.
+            shc.handshakeExtensions.remove(SSLExtension.CH_ALPN);
+
+            return extData;
+        }
+    }
+
+    /**
+     * Network data consumer of the extension in the ServerHello
+     * handshake message.
+     */
+    private static final class SHAlpnConsumer implements ExtensionConsumer {
+        // Prevent instantiation of this class.
+        private SHAlpnConsumer() {
+            // blank
+        }
+
+        @Override
+        public void consume(ConnectionContext context,
+            HandshakeMessage message, ByteBuffer buffer) throws IOException {
+            // The producing happens in client side only.
+            ClientHandshakeContext chc = (ClientHandshakeContext)context;
+
+            // In response to ALPN request only
+            AlpnSpec requestedAlps =
+                    (AlpnSpec)chc.handshakeExtensions.get(SSLExtension.CH_ALPN);
+            if (requestedAlps == null) {
+                chc.conContext.fatal(Alert.UNEXPECTED_MESSAGE,
+                    "Unexpected " + SSLExtension.CH_ALPN.name + " extension");
         }
 
-        return "Extension " + type +
-            ", protocol names: " + sb;
+            // Parse the extension.
+            AlpnSpec spec;
+            try {
+                spec = new AlpnSpec(buffer);
+            } catch (IOException ioe) {
+                chc.conContext.fatal(Alert.UNEXPECTED_MESSAGE, ioe);
+                return;     // fatal() always throws, make the compiler happy.
+            }
+
+            // Only one application protocol is allowed.
+            if (spec.applicationProtocols.size() != 1) {
+                chc.conContext.fatal(Alert.UNEXPECTED_MESSAGE,
+                    "Invalid " + SSLExtension.CH_ALPN.name + " extension: " +
+                    "Only one protocol name is allowed in ServerHello message");
+            }
+
+            // Update the context.
+            chc.applicationProtocol = spec.applicationProtocols.get(0);
+            chc.conContext.applicationProtocol = chc.applicationProtocol;
+
+            // Clean or register the extension
+            //
+            // No further use of the request and respond extension any more.
+            chc.handshakeExtensions.remove(SSLExtension.CH_ALPN);
+        }
+    }
+
+    /**
+     * The absence processing if the extension is not present in
+     * the ServerHello handshake message.
+     */
+    private static final class SHAlpnAbsence implements HandshakeAbsence {
+        @Override
+        public void absent(ConnectionContext context,
+                HandshakeMessage message) throws IOException {
+            // The producing happens in client side only.
+            ClientHandshakeContext chc = (ClientHandshakeContext)context;
+
+            // Please don't use the previous negotiated application protocol.
+            chc.applicationProtocol = "";
+            chc.conContext.applicationProtocol = "";
+        }
     }
 }
< prev index next >