/* WolfSSLAuthStore.java * * Copyright (C) 2006-2023 wolfSSL Inc. * * This file is part of wolfSSL. * * wolfSSL is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * wolfSSL 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 for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA */ package com.wolfssl.provider.jsse; import com.wolfssl.WolfSSL; import com.wolfssl.WolfSSL.TLS_VERSION; import com.wolfssl.WolfSSLSession; import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.X509KeyManager; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import javax.net.ssl.TrustManagerFactory; import java.security.KeyStore; import java.security.SecureRandom; import java.security.KeyStoreException; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.security.UnrecoverableKeyException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Collection; import java.util.Date; import java.util.Enumeration; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * Helper class used to store common settings, objects, etc. */ public class WolfSSLAuthStore { private TLS_VERSION currentVersion = TLS_VERSION.INVALID; private X509KeyManager km = null; private X509TrustManager tm = null; private SecureRandom sr = null; private String alias = null; private WolfSSLSessionContext serverCtx = null; private WolfSSLSessionContext clientCtx = null; private SessionStore store = null; private static final Object storeLock = new Object(); /** * Protected constructor to create new WolfSSLAuthStore * @param keyman key manager to use * @param trustman trust manager to use * @param random secure random * @param version TLS protocol version to use * @throws IllegalArgumentException when bad values are passed in * @throws KeyManagementException in the case that getting keys fails */ protected WolfSSLAuthStore(KeyManager[] keyman, TrustManager[] trustman, SecureRandom random, TLS_VERSION version) throws IllegalArgumentException, KeyManagementException { /* default session cache size of 33 to match native wolfSSL * default cache size */ int defaultCacheSize = 33; if (version == TLS_VERSION.INVALID) { throw new IllegalArgumentException("Invalid SSL/TLS version"); } WolfSSLDebug.log(getClass(), WolfSSLDebug.INFO, "Creating new WolfSSLAuthStore"); initKeyManager(keyman); initTrustManager(trustman); initSecureRandom(random); this.currentVersion = version; store = new SessionStore<>(defaultCacheSize); this.serverCtx = new WolfSSLSessionContext( this, defaultCacheSize, WolfSSL.WOLFSSL_SERVER_END); this.clientCtx = new WolfSSLSessionContext( this, defaultCacheSize, WolfSSL.WOLFSSL_CLIENT_END); } /** * Initialize key manager. * The first instance of X509KeyManager found will be used. If null is * passed in, installed security providers with be searched for highest * priority implementation of the required factory. */ private void initKeyManager(KeyManager[] in) throws KeyManagementException { KeyManager[] managers = in; if (managers == null || managers.length == 0) { try { WolfSSLDebug.log(getClass(), WolfSSLDebug.INFO, "searching installed providers for X509KeyManager (type: " + KeyManagerFactory.getDefaultAlgorithm() +")"); /* use key managers from installed security providers */ KeyManagerFactory kmFactory = KeyManagerFactory.getInstance( KeyManagerFactory.getDefaultAlgorithm()); kmFactory.init(null, null); managers = kmFactory.getKeyManagers(); } catch (NoSuchAlgorithmException nsae) { throw new KeyManagementException(nsae); } catch (KeyStoreException kse) { throw new KeyManagementException(kse); } catch (UnrecoverableKeyException uke) { throw new KeyManagementException(uke); } } if (managers != null) { for (int i = 0; i < managers.length; i++) { if (managers[i] instanceof X509KeyManager) { km = (X509KeyManager)managers[i]; WolfSSLDebug.log(getClass(), WolfSSLDebug.INFO, "located X509KeyManager instance: " + km); break; } } } } /** * Initialize trust manager. * The first instance of X509TrustManager found will be used. If null is * passed in, installed security providers with be searched for highest * priority implementation of the required factory. */ private void initTrustManager(TrustManager[] in) throws KeyManagementException { TrustManager[] managers = in; if (managers == null || managers.length == 0) { try { WolfSSLDebug.log(getClass(), WolfSSLDebug.INFO, "searching installed providers for X509TrustManager"); /* use trust managers from installed security providers */ TrustManagerFactory tmFactory = TrustManagerFactory.getInstance( TrustManagerFactory.getDefaultAlgorithm()); tmFactory.init((KeyStore)null); managers = tmFactory.getTrustManagers(); } catch (NoSuchAlgorithmException nsae) { throw new KeyManagementException(nsae); } catch (KeyStoreException kse) { throw new KeyManagementException(kse); } } if (managers != null) { for (int i = 0; i < managers.length; i++) { if (managers[i] instanceof X509TrustManager) { tm = (X509TrustManager)managers[i]; WolfSSLDebug.log(getClass(), WolfSSLDebug.INFO, "located X509TrustManager instance: " + tm); break; } } } } /** * Initialize secure random. * If SecureRandom passed in is null, default implementation will * be used. */ private void initSecureRandom(SecureRandom random) { if (random == null) { sr = new SecureRandom(); } sr = random; } /** * Get X509KeyManager for this object * @return get the key manager used */ protected X509KeyManager getX509KeyManager() { return this.km; } /** * Get X509TrustManager for this object * @return get the trust manager used */ protected X509TrustManager getX509TrustManager() { return this.tm; } /** * Get the SecureRandom for this object * @return get secure random */ protected SecureRandom getSecureRandom() { return this.sr; } /** * Get protocol version set * @return get the current protocol version set */ protected TLS_VERSION getProtocolVersion() { return this.currentVersion; } /** * Set certificate alias * @param in alias to set for certificate used */ protected void setCertAlias(String in) { this.alias = in; } /** * Get certificate alias * @return alias name */ protected String getCertAlias() { return this.alias; } /** * Getter function for WolfSSLSessionContext associated with store * @return pointer to the context set */ protected WolfSSLSessionContext getServerContext() { return this.serverCtx; } /** * Getter function for WolfSSLSessionContext associated with store * @return pointer to the context set */ protected WolfSSLSessionContext getClientContext() { return this.clientCtx; } /** * Reset the size of the array to cache sessions * @param sz new array size * @param side server/client side for cache resize */ protected void resizeCache(int sz, int side) { SessionStore newStore = new SessionStore<>(sz); //@TODO check for side server/client, currently a resize is for all synchronized (storeLock) { store.putAll(newStore); store = newStore; } } /** * Get and return either an existing session from the Java session cache * table, or create a new session if one does not exist. * * This method can return null if ether an error occurs getting a session, * or a new session could not be created. * * If called on the server side (clientMode == false), a new * WolfSSLImplementSSLSession will be created and returned, since the * server-side session cache is managed and maintained interal to native * wolfSSL. * * @param ssl WolfSSLSession (WOLFSSL) for which the returned session * is stored back into (ie: wolfSSL_set_session(ssl, session)) * @param port port number of peer being connected to * @param host host of the peer being connected to * @param clientMode if is client side then true, otherwise false * @return an existing SSLSession from Java session cache, or a new * object if not in cache, called on server side, or host * is null */ protected synchronized WolfSSLImplementSSLSession getSession( WolfSSLSession ssl, int port, String host, boolean clientMode) { WolfSSLImplementSSLSession ses = null; String toHash = null; if (ssl == null) { return null; } /* Return new session if in server mode, or if host is null */ if (!clientMode || host == null) { return this.getSession(ssl); } WolfSSLDebug.log(getClass(), WolfSSLDebug.INFO, "attempting to look up session (" + "host: " + host + ", port: " + port + ")"); /* Print current size and contents of SessionStore / LinkedHashMap. * Synchronizes on storeLock internally. */ printSessionStoreStatus(); /* Lock on static/global storeLock, since Java session cache table * is shared between all threads */ synchronized (storeLock) { /* generate cache key hash (host:port) */ toHash = host.concat(Integer.toString(port)); /* try getting session out of Java store */ ses = store.get(toHash.hashCode()); if (ses == null) { WolfSSLDebug.log(getClass(), WolfSSLDebug.INFO, "session not found in cache table, creating new"); /* not found in stored sessions create a new one */ ses = new WolfSSLImplementSSLSession(ssl, port, host, this); ses.setValid(true); /* new sessions marked as valid */ ses.isFromTable = false; ses.setPseudoSessionId( Integer.toString(ssl.hashCode()).getBytes()); } else { /* Remove old entry from table. TLS 1.3 binder changes between * resumptions and stored session should only be used to * resume once. New session structure/object will be cached * after the resumed session completes the handshake, for * subsequent resumption attempts to use. */ store.remove(toHash.hashCode()); ses.isFromTable = true; WolfSSLDebug.log(getClass(), WolfSSLDebug.INFO, "session found in cache, trying to resume"); ses.resume(ssl); } return ses; } } /** * Print summary of current SessionStore (LinkedHashMap) status. * Prints out size of current SessionStore. If size is greater than zero, * prints out host:port of all sessions stored in the store. * Called by getSession(). */ private void printSessionStoreStatus() { synchronized (storeLock) { Collection values = store.values(); WolfSSLDebug.log(getClass(), WolfSSLDebug.INFO, "SessionStore Status : --------------------------"); WolfSSLDebug.log(getClass(), WolfSSLDebug.INFO, " size: " + store.size()); if (store.size() > 0) { WolfSSLDebug.log(getClass(), WolfSSLDebug.INFO, " values: "); for (WolfSSLImplementSSLSession s : values) { WolfSSLDebug.log(getClass(), WolfSSLDebug.INFO, " " + s.getHost() + ": " + s.getPort()); } } WolfSSLDebug.log(getClass(), WolfSSLDebug.INFO, "------------------------------------------------"); } } /** Returns a new session, does not check/save for resumption * @param ssl WOLFSSL class to reference with new session * @return a new SSLSession on success */ protected synchronized WolfSSLImplementSSLSession getSession( WolfSSLSession ssl) { WolfSSLDebug.log(getClass(), WolfSSLDebug.INFO, "creating new session"); WolfSSLImplementSSLSession ses = new WolfSSLImplementSSLSession(ssl, this); ses.setValid(true); ses.isFromTable = false; ses.setPseudoSessionId(Integer.toString(ssl.hashCode()).getBytes()); return ses; } /** * Internal helper function to check if session ID is all zeros. * Used by addSession() * * @param id session ID * @return true if array is all zeros (0x00), otherwise false */ private boolean idAllZeros(byte[] id) { boolean ret = true; if (id == null) { return true; } for (int i = 0; i < id.length; i++) { if (id[i] != 0x00) { return false; } } return true; } /** * Add SSLSession into wolfJSSE Java session cache table, to be used * for session resumption. * * Session is stored into the session table using a hash code as the key. * If the peer host is not null, the hash code is based on a concatenation * of the peer host and port. If the peer host is null, the hash code * is based on the session ID (if ID is not null, and non-zero length). * Otherwise, no hash code is generated and the session is not stored into * the session cache table. * * This method synchronizes on the static/global storeLock object, since * the session cache is global and shared amongst all threads. * * @param session SSLSession to be stored in Java session cache * @return SSL_SUCCESS on success */ protected int addSession(WolfSSLImplementSSLSession session) { String toHash; int hashCode = 0; /* Lock access to store while adding new session, store is global */ synchronized (storeLock) { if (session.getPeerHost() != null) { /* Generate key for storing into session table (host:port) */ toHash = session.getPeerHost().concat(Integer.toString( session.getPeerPort())); hashCode = toHash.hashCode(); } else { /* If no peer host is available then create hash key from * session ID if not null, not zero length, and not all zeros */ byte[] sessionId = session.getId(); if (sessionId != null && sessionId.length > 0 && (idAllZeros(sessionId) == false)) { hashCode = Arrays.toString(session.getId()).hashCode(); } } /* Always try to store session into cache table, as long as we * have a hashCode. If session already exists for hashCode, it * will be overwritten with new/refreshed version */ if (hashCode != 0) { WolfSSLDebug.log(getClass(), WolfSSLDebug.INFO, "stored session in cache table (host: " + session.getPeerHost() + ", port: " + session.getPeerPort() + ") " + "hashCode = " + hashCode + " side = " + session.getSide()); store.put(hashCode, session); session.isInTable = true; } } return WolfSSL.SSL_SUCCESS; } /** * Internal function to return a list of all session ID's * @param side server or client side to get list of ID's from * @return enumerated session IDs */ protected Enumeration getAllIDs(int side) { List ret = new ArrayList<>(); synchronized (storeLock) { for (Object obj : store.values()) { WolfSSLImplementSSLSession current = (WolfSSLImplementSSLSession)obj; if (current.getSide() == side) { ret.add(current.getId()); } } return Collections.enumeration(ret); } } /** * Getter function for session with session id 'ID' * @param ID the session id to search for * @param side if the session is expected on the server or client side * @return session from the store that has session id 'ID' */ protected WolfSSLImplementSSLSession getSession(byte[] ID, int side) { WolfSSLImplementSSLSession ret = null; synchronized (storeLock) { for (Object obj : store.values()) { WolfSSLImplementSSLSession current = (WolfSSLImplementSSLSession)obj; if (current.getSide() == side && java.util.Arrays.equals(ID, current.getId())) { ret = current; break; } } return ret; } } /** * Goes through the list of sessions and checks for timeouts. If timed out * then the session is invalidated. * @param in the updated timeout value to check against * @param side server or client side getting the timeout update */ protected void updateTimeouts(int in, int side) { Date currentDate = new Date(); long now = currentDate.getTime(); synchronized (storeLock) { for (Object obj : store.values()) { long diff; WolfSSLImplementSSLSession current = (WolfSSLImplementSSLSession)obj; if (current.getSide() == side) { /* difference in seconds */ diff = (now - current.creation.getTime()) / 1000; if (diff < 0) { /* session is from the future ... */ //@TODO } if (in > 0 && diff > in) { current.invalidate(); } current.setNativeTimeout(in); } } } } private class SessionStore extends LinkedHashMap { /** * user defined ID */ private static final long serialVersionUID = 1L; private final int maxSz; /** * @param in max size of hash map before oldest entry is overwritten */ protected SessionStore(int in) { maxSz = in; } @Override protected boolean removeEldestEntry(Map.Entry oldest) { return size() > maxSz; } } @SuppressWarnings("deprecation") @Override protected synchronized void finalize() throws Throwable { /* Clear LinkedHashMap and set to null to allow * for garbage collection */ store.clear(); store = null; super.finalize(); } }