/* * Copyright (c) 1999, 2024, 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.util.ArrayList; import java.util.Collections; import java.util.Enumeration; import java.util.Iterator; import java.util.Locale; import java.util.Map; import java.util.Random; import java.util.concurrent.ConcurrentHashMap; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSessionContext; import sun.security.action.GetIntegerAction; import sun.security.action.GetPropertyAction; import sun.security.util.Cache; /** * {@systemProperty jdk.tls.server.enableSessionTicketExtension} determines if the * server will provide stateless session tickets, if the client supports it, * as described in RFC 5077 and RFC 8446. a stateless session ticket * contains the encrypted server's state which saves server resources. * * {@systemProperty jdk.tls.client.enableSessionTicketExtension} determines if the * client will send an extension in the ClientHello in the pre-TLS 1.3. * This extension allows the client to accept the server's session state for * Server Side stateless resumption (RFC 5077). Setting the property to * "false" turns this off, by default it is true. For TLS 1.3, the system * property is not needed as this support is part of the spec. * * {@systemProperty jdk.tls.server.sessionTicketTimeout} determines how long * a session in the server cache or the stateless resumption tickets are * available for use. The value set by the property can be modified by * {@code SSLSessionContext.setSessionTimeout()} during runtime. * */ final class SSLSessionContextImpl implements SSLSessionContext { private static final int DEFAULT_MAX_CACHE_SIZE = 20480; private static final int DEFAULT_MAX_QUEUE_SIZE = 10; // Default lifetime of a session. 24 hours static final int DEFAULT_SESSION_TIMEOUT = 86400; private final Cache sessionCache; // session cache, session id as key private final Cache sessionHostPortCache; // session cache, "host:port" as key private int cacheLimit; // the max cache size private int timeout; // timeout in seconds // The current session ticket encryption key ID (only used in server context) private int currentKeyID; // Session ticket encryption keys and IDs map (only used in server context) private final Map keyHashMap; // Default setting for stateless session resumption support (RFC 5077) private boolean statelessSession = true; // package private SSLSessionContextImpl(boolean server) { timeout = DEFAULT_SESSION_TIMEOUT; cacheLimit = getDefaults(server); // default cache size // use soft reference if (server) { sessionCache = Cache.newSoftMemoryCache(cacheLimit, timeout); sessionHostPortCache = Cache.newSoftMemoryCache(cacheLimit, timeout); keyHashMap = new ConcurrentHashMap<>(); // Should be "randomly generated" according to RFC 5077, // but doesn't necessarily have to be a true random number. currentKeyID = new Random(System.nanoTime()).nextInt(); } else { sessionCache = Cache.newSoftMemoryCache(cacheLimit, timeout); sessionHostPortCache = Cache.newSoftMemoryQueue(cacheLimit, timeout, DEFAULT_MAX_QUEUE_SIZE); keyHashMap = Map.of(); } } // Stateless sessions when available, but there is a cache boolean statelessEnabled() { return statelessSession; } /** * Returns the SSLSession bound to the specified session id. */ @Override public SSLSession getSession(byte[] sessionId) { if (sessionId == null) { throw new NullPointerException("session id cannot be null"); } SSLSessionImpl sess = sessionCache.get(new SessionId(sessionId)); if (!isTimedout(sess)) { return sess; } return null; } /** * Returns an enumeration of the active SSL sessions. */ @Override public Enumeration getIds() { SessionCacheVisitor scVisitor = new SessionCacheVisitor(); sessionCache.accept(scVisitor); return scVisitor.getSessionIds(); } /** * Sets the timeout limit for cached SSLSession objects * * Note that after reset the timeout, the cached session before * should be timed within the shorter one of the old timeout and the * new timeout. */ @Override public void setSessionTimeout(int seconds) throws IllegalArgumentException { if (seconds < 0) { throw new IllegalArgumentException(); } if (timeout != seconds) { sessionCache.setTimeout(seconds); sessionHostPortCache.setTimeout(seconds); timeout = seconds; } } /** * Gets the timeout limit for cached SSLSession objects */ @Override public int getSessionTimeout() { return timeout; } /** * Sets the size of the cache used for storing * SSLSession objects. */ @Override public void setSessionCacheSize(int size) throws IllegalArgumentException { if (size < 0) throw new IllegalArgumentException(); if (cacheLimit != size) { sessionCache.setCapacity(size); sessionHostPortCache.setCapacity(size); cacheLimit = size; } } /** * Gets the size of the cache used for storing * SSLSession objects. */ @Override public int getSessionCacheSize() { return cacheLimit; } private void cleanupStatelessKeys() { Iterator> it = keyHashMap.entrySet().iterator(); while (it.hasNext()) { Map.Entry entry = it.next(); SessionTicketExtension.StatelessKey k = entry.getValue(); if (k.isInvalid(this)) { it.remove(); try { k.key.destroy(); } catch (Exception e) { // Suppress } } } } // Package-private, used only from SessionTicketExtension.KeyState::getCurrentKey. SessionTicketExtension.StatelessKey getKey(HandshakeContext hc) { SessionTicketExtension.StatelessKey ssk = keyHashMap.get(currentKeyID); if (ssk != null && !ssk.isExpired()) { return ssk; } synchronized (this) { // If the current key is no longer expired, it was already // updated by a concurrent request, and we can return. ssk = keyHashMap.get(currentKeyID); if (ssk != null && !ssk.isExpired()) { return ssk; } int newID = currentKeyID + 1; ssk = new SessionTicketExtension.StatelessKey(hc, newID); keyHashMap.put(Integer.valueOf(newID), ssk); currentKeyID = newID; } // Check for and delete invalid keys every time we create a new stateless key. cleanupStatelessKeys(); return ssk; } // Package-private, used only from SessionTicketExtension.KeyState::getKey. SessionTicketExtension.StatelessKey getKey(int id) { return keyHashMap.get(id); } // package-private method, used ONLY by ServerHandshaker SSLSessionImpl get(byte[] id) { return (SSLSessionImpl)getSession(id); } // package-private method, find and remove session from cache // return found session SSLSessionImpl pull(byte[] id) { if (id != null) { return sessionCache.pull(new SessionId(id)); } return null; } // package-private method, used ONLY by ClientHandshaker SSLSessionImpl get(String hostname, int port) { /* * If no session caching info is available, we won't * get one, so exit before doing a lookup. */ if (hostname == null && port == -1) { return null; } SSLSessionImpl sess = sessionHostPortCache.get(getKey(hostname, port)); if (!isTimedout(sess)) { return sess; } return null; } private static String getKey(String hostname, int port) { return (hostname + ":" + port).toLowerCase(Locale.ENGLISH); } // cache a SSLSession // // In SunJSSE implementation, a session is created while getting a // client hello or a server hello message, and cached while the // handshaking finished. // Here we time the session from the time it cached instead of the // time it created, which is a little longer than the expected. So // please do check isTimedout() while getting entry from the cache. void put(SSLSessionImpl s) { put(s, false); } /** * Put an entry in the cache * @param s SSLSessionImpl entry to be stored * @param canQueue True if multiple entries may exist under one * session entry. */ void put(SSLSessionImpl s, boolean canQueue) { sessionCache.put(s.getSessionId(), s); // If no hostname/port info is available, don't add this one. if ((s.getPeerHost() != null) && (s.getPeerPort() != -1)) { sessionHostPortCache.put( getKey(s.getPeerHost(), s.getPeerPort()), s, canQueue); } s.setContext(this); } // package-private method, remove a cached SSLSession void remove(SessionId key) { remove(key, false); } void remove(SessionId key, boolean isClient) { SSLSessionImpl s = sessionCache.get(key); if (s != null) { sessionCache.remove(key); // A client keeps the cache entry for queued NST resumption. if (!isClient) { sessionHostPortCache.remove( getKey(s.getPeerHost(), s.getPeerPort())); } } } private int getDefaults(boolean server) { try { String st; // Property for Session Cache state if (server) { st = GetPropertyAction.privilegedGetProperty( "jdk.tls.server.enableSessionTicketExtension", "true"); } else { st = GetPropertyAction.privilegedGetProperty( "jdk.tls.client.enableSessionTicketExtension", "true"); } if (st.compareToIgnoreCase("false") == 0) { statelessSession = false; } // Property for Session Ticket Timeout. The value can be changed // by SSLSessionContext.setSessionTimeout(int) String s = GetPropertyAction.privilegedGetProperty( "jdk.tls.server.sessionTicketTimeout"); if (s != null) { try { int t = Integer.parseInt(s); if (t < 0 || t > NewSessionTicket.MAX_TICKET_LIFETIME) { timeout = DEFAULT_SESSION_TIMEOUT; if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { SSLLogger.warning("Invalid timeout given " + "jdk.tls.server.sessionTicketTimeout: " + t + ". Set to default value " + timeout); } } else { timeout = t; } } catch (NumberFormatException e) { setSessionTimeout(DEFAULT_SESSION_TIMEOUT); if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { SSLLogger.warning("Invalid timeout for " + "jdk.tls.server.sessionTicketTimeout: " + s + ". Set to default value " + timeout); } } } int defaultCacheLimit = GetIntegerAction.privilegedGetProperty( "javax.net.ssl.sessionCacheSize", DEFAULT_MAX_CACHE_SIZE); if (defaultCacheLimit >= 0) { return defaultCacheLimit; } else if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { SSLLogger.warning( "invalid System Property javax.net.ssl.sessionCacheSize, " + "use the default session cache size (" + DEFAULT_MAX_CACHE_SIZE + ") instead"); } } catch (Exception e) { // unlikely, log it for safe if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { SSLLogger.warning( "the System Property javax.net.ssl.sessionCacheSize is " + "not available, use the default value (" + DEFAULT_MAX_CACHE_SIZE + ") instead"); } } return DEFAULT_MAX_CACHE_SIZE; } private boolean isTimedout(SSLSession sess) { if (timeout == 0) { return false; } if ((sess != null) && ((sess.getCreationTime() + timeout * 1000L) <= (System.currentTimeMillis()))) { sess.invalidate(); return true; } return false; } private final class SessionCacheVisitor implements Cache.CacheVisitor { ArrayList ids = null; // public void visit(java.util.Map map) {} @Override public void visit(java.util.Map map) { ids = new ArrayList<>(map.size()); for (SessionId key : map.keySet()) { SSLSessionImpl value = map.get(key); if (!isTimedout(value)) { ids.add(key.getId()); } } } Enumeration getSessionIds() { return ids != null ? Collections.enumeration(ids) : Collections.emptyEnumeration(); } } }