src/java.base/share/classes/sun/security/ssl/SessionTicketExtension.java
branchdatagramsocketimpl-branch
changeset 58678 9cf78a70fa4f
child 58679 9c3209ff7550
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/java.base/share/classes/sun/security/ssl/SessionTicketExtension.java	Thu Oct 17 20:53:35 2019 +0100
@@ -0,0 +1,544 @@
+/*
+ * Copyright (c) 2019, 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 sun.security.action.GetPropertyAction;
+import sun.security.ssl.SSLExtension.ExtensionConsumer;
+import sun.security.ssl.SSLExtension.SSLExtensionSpec;
+import sun.security.ssl.SSLHandshake.HandshakeMessage;
+import sun.security.ssl.SupportedGroupsExtension.SupportedGroups;
+import sun.security.util.HexDumpEncoder;
+
+import javax.crypto.Cipher;
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.net.ssl.SSLProtocolException;
+
+import static sun.security.ssl.SSLExtension.CH_SESSION_TICKET;
+import static sun.security.ssl.SSLExtension.SH_SESSION_TICKET;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.text.MessageFormat;
+import java.util.Locale;
+
+/**
+ * SessionTicketExtension is an implementation of RFC 5077 with some internals
+ * that are used for stateless operation in TLS 1.3.
+ *
+ * {@systemProperty jdk.tls.server.statelessKeyTimeout} can override the default
+ * amount of time, in seconds, for how long a randomly-generated key and
+ * parameters can be used before being regenerated.  The key material is used
+ * to encrypt the stateless session ticket that is sent to the client that will
+ * be used during resumption.  Default is 3600 seconds (1 hour)
+ *
+ */
+
+final class SessionTicketExtension {
+
+    static final HandshakeProducer chNetworkProducer =
+            new T12CHSessionTicketProducer();
+    static final ExtensionConsumer chOnLoadConsumer =
+            new T12CHSessionTicketConsumer();
+    static final HandshakeProducer shNetworkProducer =
+            new T12SHSessionTicketProducer();
+    static final ExtensionConsumer shOnLoadConsumer =
+            new T12SHSessionTicketConsumer();
+
+    static final SSLStringizer steStringizer = new SessionTicketStringizer();
+
+    // Time in milliseconds until key is changed for encrypting session state
+    private static final int TIMEOUT_DEFAULT = 3600 * 1000;
+    private static final int keyTimeout;
+    private static int currentKeyID = new SecureRandom().nextInt();
+    private static final int KEYLEN = 256;
+
+    static {
+        String s = GetPropertyAction.privilegedGetProperty(
+                "jdk.tls.server.statelessKeyTimeout");
+        if (s != null) {
+            int kt;
+            try {
+                kt = Integer.parseInt(s) * 1000;  // change to ms
+                if (kt < 0 ||
+                        kt > NewSessionTicket.MAX_TICKET_LIFETIME) {
+                    if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
+                        SSLLogger.warning("Invalid timeout for " +
+                                "jdk.tls.server.statelessKeyTimeout: " +
+                                kt + ".  Set to default value " +
+                                TIMEOUT_DEFAULT + "sec");
+                    }
+                    kt = TIMEOUT_DEFAULT;
+                }
+            } catch (NumberFormatException e) {
+                kt = TIMEOUT_DEFAULT;
+                if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
+                    SSLLogger.warning("Invalid timeout for " +
+                            "jdk.tls.server.statelessKeyTimeout: " + s +
+                            ".  Set to default value " + TIMEOUT_DEFAULT +
+                            "sec");
+                }
+            }
+            keyTimeout = kt;
+        } else {
+            keyTimeout = TIMEOUT_DEFAULT;
+        }
+    }
+
+    // Crypto key context for session state.  Used with stateless operation.
+    final static class StatelessKey {
+        final long timeout;
+        final SecretKey key;
+        final int num;
+
+        StatelessKey(HandshakeContext hc, int newNum) {
+            SecretKey k = null;
+            try {
+                KeyGenerator kg = KeyGenerator.getInstance("AES");
+                kg.init(KEYLEN, hc.sslContext.getSecureRandom());
+                k = kg.generateKey();
+            } catch (NoSuchAlgorithmException e) {
+                // should not happen;
+            }
+            key = k;
+            timeout = System.currentTimeMillis() + keyTimeout;
+            num = newNum;
+            hc.sslContext.keyHashMap.put(Integer.valueOf(num), this);
+        }
+
+        // Check if key needs to be changed
+        boolean isExpired() {
+            return ((System.currentTimeMillis()) > timeout);
+        }
+
+        // Check if this key is ready for deletion.
+        boolean isInvalid(long sessionTimeout) {
+            return ((System.currentTimeMillis()) > (timeout + sessionTimeout));
+        }
+    }
+
+    private static final class KeyState {
+
+        // Get a key with a specific key number
+        static StatelessKey getKey(HandshakeContext hc, int num)  {
+            StatelessKey ssk = hc.sslContext.keyHashMap.get(num);
+
+            if (ssk == null || ssk.isInvalid(getSessionTimeout(hc))) {
+                return null;
+            }
+            return ssk;
+        }
+
+        // Get the current valid key, this will generate a new key if needed
+        static StatelessKey getCurrentKey(HandshakeContext hc) {
+            StatelessKey ssk = hc.sslContext.keyHashMap.get(currentKeyID);
+
+            if (ssk != null && !ssk.isExpired()) {
+                return ssk;
+            }
+            return nextKey(hc);
+        }
+
+        // This method locks when the first getCurrentKey() finds it to be too
+        // old and create a new key to replace the current key.  After the new
+        // key established, the lock can be released so following
+        // operations will start using the new key.
+        // The first operation will take a longer code path by generating the
+        // next key and cleaning up old keys.
+        private static StatelessKey nextKey(HandshakeContext hc) {
+            StatelessKey ssk;
+
+            synchronized (hc.sslContext.keyHashMap) {
+                // If the current key is no longer expired, it was already
+                // updated by a previous operation and we can return.
+                ssk = hc.sslContext.keyHashMap.get(currentKeyID);
+                if (ssk != null && !ssk.isExpired()) {
+                    return ssk;
+                }
+                int newNum;
+                if (currentKeyID == Integer.MAX_VALUE) {
+                    newNum = 0;
+                } else {
+                    newNum = currentKeyID + 1;
+                }
+                // Get new key
+                ssk = new StatelessKey(hc, newNum);
+                currentKeyID = newNum;
+                // Release lock since the new key is ready to be used.
+            }
+
+            // Clean up any old keys, then return the current key
+            cleanup(hc);
+            return ssk;
+        }
+
+        // Deletes any invalid SessionStateKeys.
+        static void cleanup(HandshakeContext hc) {
+            int sessionTimeout = getSessionTimeout(hc);
+
+            StatelessKey ks;
+            for (Object o : hc.sslContext.keyHashMap.keySet().toArray()) {
+                Integer i = (Integer)o;
+                ks = hc.sslContext.keyHashMap.get(i);
+                if (ks.isInvalid(sessionTimeout)) {
+                    try {
+                        ks.key.destroy();
+                    } catch (Exception e) {
+                        // Suppress
+                    }
+                    hc.sslContext.keyHashMap.remove(i);
+                }
+            }
+        }
+
+        static int getSessionTimeout(HandshakeContext hc) {
+            return hc.sslContext.engineGetServerSessionContext().
+                    getSessionTimeout() * 1000;
+        }
+    }
+
+    /**
+     * This class contains the session state that is in the session ticket.
+     * Using the key associated with the ticket, the class encrypts and
+     * decrypts the data, but does not interpret the data.
+     */
+    static final class SessionTicketSpec implements SSLExtensionSpec {
+        private static final int GCM_TAG_LEN = 128;
+        ByteBuffer data;
+        static final ByteBuffer zero = ByteBuffer.wrap(new byte[0]);
+
+        SessionTicketSpec() {
+            data = zero;
+        }
+
+        SessionTicketSpec(byte[] b) throws IOException {
+            this(ByteBuffer.wrap(b));
+        }
+
+        SessionTicketSpec(ByteBuffer buf) throws IOException {
+            if (buf == null) {
+                throw new SSLProtocolException(
+                        "SessionTicket buffer too small");
+            }
+            if (buf.remaining() > 65536) {
+                throw new SSLProtocolException(
+                        "SessionTicket buffer too large. " + buf.remaining());
+            }
+
+            data = buf;
+        }
+
+        public byte[] encrypt(HandshakeContext hc, SSLSessionImpl session) {
+            byte[] encrypted;
+
+            if (!hc.handshakeSession.isStatelessable(hc)) {
+                return new byte[0];
+            }
+
+            try {
+                StatelessKey key = KeyState.getCurrentKey(hc);
+                byte[] iv = new byte[16];
+
+                SecureRandom random = hc.sslContext.getSecureRandom();
+                random.nextBytes(iv);
+                Cipher c = Cipher.getInstance("AES/GCM/NoPadding");
+                c.init(Cipher.ENCRYPT_MODE, key.key,
+                        new GCMParameterSpec(GCM_TAG_LEN, iv));
+                c.updateAAD(new byte[] {
+                        (byte)(key.num >>> 24),
+                        (byte)(key.num >>> 16),
+                        (byte)(key.num >>> 8),
+                        (byte)(key.num)}
+                );
+                byte[] data = session.write();
+                if (data.length == 0) {
+                    return data;
+                }
+                encrypted = c.doFinal(data);
+                byte[] result = new byte[encrypted.length + Integer.BYTES +
+                        iv.length];
+                result[0] = (byte)(key.num >>> 24);
+                result[1] = (byte)(key.num >>> 16);
+                result[2] = (byte)(key.num >>> 8);
+                result[3] = (byte)(key.num);
+                System.arraycopy(iv, 0, result, Integer.BYTES, iv.length);
+                System.arraycopy(encrypted, 0, result,
+                        Integer.BYTES + iv.length, encrypted.length);
+                return result;
+            } catch (Exception e) {
+                if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
+                    SSLLogger.fine("Encryption failed." + e);
+                }
+                return new byte[0];
+            }
+        }
+
+        ByteBuffer decrypt(HandshakeContext hc) {
+            int keyID;
+            byte[] iv;
+            try {
+                keyID = data.getInt();
+                StatelessKey key = KeyState.getKey(hc, keyID);
+                if (key == null) {
+                    return null;
+                }
+
+                iv = new byte[16];
+                data.get(iv);
+                Cipher c = Cipher.getInstance("AES/GCM/NoPadding");
+                c.init(Cipher.DECRYPT_MODE, key.key,
+                        new GCMParameterSpec(GCM_TAG_LEN, iv));
+                c.updateAAD(new byte[] {
+                        (byte)(keyID >>> 24),
+                        (byte)(keyID >>> 16),
+                        (byte)(keyID >>> 8),
+                        (byte)(keyID)}
+                );
+
+                ByteBuffer out;
+                out = ByteBuffer.allocate(data.remaining() - GCM_TAG_LEN / 8);
+                c.doFinal(data, out);
+                out.flip();
+                return out;
+            } catch (Exception e) {
+                if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
+                    SSLLogger.fine("Decryption failed." + e.getMessage());
+                }
+            }
+            return null;
+        }
+
+        byte[] getEncoded() {
+            byte[] out = new byte[data.capacity()];
+            data.duplicate().get(out);
+            return out;
+        }
+
+        @Override
+        public String toString() {
+            if (data == null) {
+                return "<null>";
+            }
+            if (data.capacity() == 0) {
+                return "<empty>";
+            }
+
+            MessageFormat messageFormat = new MessageFormat(
+                    "  \"ticket\" : '{'\n" +
+                            "{0}\n" +
+                            "  '}'",
+                    Locale.ENGLISH);
+            HexDumpEncoder hexEncoder = new HexDumpEncoder();
+
+            Object[] messageFields = {
+                    Utilities.indent(hexEncoder.encode(data.duplicate()),
+                            "    "),
+            };
+
+            return messageFormat.format(messageFields);
+        }
+    }
+
+    static final class SessionTicketStringizer implements SSLStringizer {
+        SessionTicketStringizer() {}
+
+        @Override
+        public String toString(ByteBuffer buffer) {
+            try {
+                return new SessionTicketSpec(buffer).toString();
+            } catch (IOException e) {
+                return e.getMessage();
+            }
+        }
+    }
+
+    private static final class T12CHSessionTicketProducer
+            extends SupportedGroups implements HandshakeProducer {
+        T12CHSessionTicketProducer() {
+        }
+
+        @Override
+        public byte[] produce(ConnectionContext context,
+                HandshakeMessage message) throws IOException {
+
+            ClientHandshakeContext chc = (ClientHandshakeContext)context;
+
+            // If the context does not allow stateless tickets, exit
+            if (!((SSLSessionContextImpl)chc.sslContext.
+                    engineGetClientSessionContext()).statelessEnabled()) {
+                if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
+                    SSLLogger.fine("Stateless resumption not supported");
+                }
+                return null;
+            }
+
+            chc.statelessResumption = true;
+
+            // If resumption is not in progress, return an empty value
+            if (!chc.isResumption || chc.resumingSession == null) {
+                if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
+                    SSLLogger.fine("Stateless resumption supported");
+                }
+                return new SessionTicketSpec().getEncoded();
+            }
+
+            if (chc.localSupportedSignAlgs == null) {
+                chc.localSupportedSignAlgs =
+                        SignatureScheme.getSupportedAlgorithms(
+                                chc.algorithmConstraints, chc.activeProtocols);
+            }
+
+            return chc.resumingSession.getPskIdentity();
+        }
+
+    }
+
+    private static final class T12CHSessionTicketConsumer
+            implements ExtensionConsumer {
+        T12CHSessionTicketConsumer() {
+        }
+
+        @Override
+        public void consume(ConnectionContext context,
+                HandshakeMessage message, ByteBuffer buffer)
+                throws IOException {
+            ServerHandshakeContext shc = (ServerHandshakeContext) context;
+
+            // Skip if extension is not provided
+            if (!shc.sslConfig.isAvailable(CH_SESSION_TICKET)) {
+                return;
+            }
+
+            // Skip consumption if we are already in stateless resumption
+            if (shc.statelessResumption) {
+                return;
+            }
+            // If the context does not allow stateless tickets, exit
+            SSLSessionContextImpl cache = (SSLSessionContextImpl)shc.sslContext
+                    .engineGetServerSessionContext();
+            if (!cache.statelessEnabled()) {
+                return;
+            }
+
+            if (buffer.remaining() == 0) {
+                shc.statelessResumption = true;
+                if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
+                    SSLLogger.fine("Client accepts session tickets.");
+                }
+                return;
+            }
+
+            // Parse the extension.
+            SessionTicketSpec spec;
+            try {
+                 spec = new SessionTicketSpec(buffer);
+            } catch (IOException | RuntimeException e) {
+                if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
+                    SSLLogger.fine("SessionTicket data invalid. Doing full " +
+                            "handshake.");
+                }
+                return;
+            }
+            ByteBuffer b = spec.decrypt(shc);
+            if (b != null) {
+                shc.resumingSession = new SSLSessionImpl(shc, b);
+                shc.isResumption = true;
+                shc.statelessResumption = true;
+                if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
+                    SSLLogger.fine("Valid stateless session ticket found");
+                }
+            }
+        }
+    }
+
+
+    private static final class T12SHSessionTicketProducer
+            extends SupportedGroups implements HandshakeProducer {
+        T12SHSessionTicketProducer() {
+        }
+
+        @Override
+        public byte[] produce(ConnectionContext context,
+                HandshakeMessage message) {
+
+            ServerHandshakeContext shc = (ServerHandshakeContext)context;
+
+            // If boolean is false, the CH did not have this extension
+            if (!shc.statelessResumption) {
+                return null;
+            }
+            // If the client has sent a SessionTicketExtension and stateless
+            // is enabled on the server, return an empty message.
+            // If the context does not allow stateless tickets, exit
+            SSLSessionContextImpl cache = (SSLSessionContextImpl)shc.sslContext
+                    .engineGetServerSessionContext();
+            if (cache.statelessEnabled()) {
+                return new byte[0];
+            }
+
+            shc.statelessResumption = false;
+            return null;
+        }
+    }
+
+    private static final class T12SHSessionTicketConsumer
+            implements ExtensionConsumer {
+        T12SHSessionTicketConsumer() {
+        }
+
+        @Override
+        public void consume(ConnectionContext context,
+                HandshakeMessage message, ByteBuffer buffer)
+                throws IOException {
+            ClientHandshakeContext chc = (ClientHandshakeContext) context;
+
+            // Skip if extension is not provided
+            if (!chc.sslConfig.isAvailable(SH_SESSION_TICKET)) {
+                chc.statelessResumption = false;
+                return;
+            }
+
+            // If the context does not allow stateless tickets, exit
+            if (!((SSLSessionContextImpl)chc.sslContext.
+                    engineGetClientSessionContext()).statelessEnabled()) {
+                chc.statelessResumption = false;
+                return;
+            }
+
+            try {
+                if (new SessionTicketSpec(buffer) == null) {
+                    return;
+                }
+                chc.statelessResumption = true;
+            } catch (IOException e) {
+                throw chc.conContext.fatal(Alert.UNEXPECTED_MESSAGE, e);
+            }
+        }
+    }
+}