TransportSecurity.java

/*
** Module   : TransportSecurity.java
** Abstract : handles JSSE setup of a custom KeyManager and TrustManager
**
** Copyright (c) 2005-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ----------------------------Description-----------------------------
** 001 NVS 20050208   @19718 Created the file and completed the initial
**                           implementation. Certificates that should be
**                           located in the directory, are temporarily
**                           loaded from a file based truststore.
** 002 NVS 20050303   @20254 Added new method getTrustStore. Needed in
**                           SecurityManager class, authenticateClient
**                           method to verify the server's certificate in
**                           case DNS based name verification fails.
**                           Server side trust store is now built in
**                           SecurityCache class.
** 003 NVS 20050303   @24615 Implemented truly anonymous (no certificate)
**                           connections support with the server's
**                           certificate validation now optional.
** 004 GES 20061221   @31801 Refactoring and massive code cleanup. Removed
**                           dependencies on AuthUIHelper.
** 005 JJC 20090208   @41407 Added a special trust manager for the use
**                           in the admin applet exclusively -
**                           ZeroActionTrustManager.
** 006 LMR 20101209          TransportSecurity's constructor was modified
**                           to bypass all logic related to keystores and
**                           certificates if net/connection/secure is false 
**                           in the BootstrapConfig (if not present it
**                           defaults to false).
** 007 GES 20111003          Make the ZeroActionTrustManager more easily
**                           accessible via a specific config value.
** 008 CA  20140206          Changed to allow the key and trust stores to be read from the 
**                           bootstrap config (needed by launching the processes via the spawner
**                           and scheduler infrastructure).
** 009 ECF 20150828          Enable Java 8.
** 010 CA  20180608          Extracted the key store loading into a separate API - loadKeyStore.
**                           This loads just the certificate, not the actual private keys.
** 011 GBB 20230825          JavaDoc updated.
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program 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 Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
** 
** Additional terms under GNU Affero GPL version 3 section 7:
** 
**   Under Section 7 of the GNU Affero GPL version 3, the following additional
**   terms apply to the works covered under the License.  These additional terms
**   are non-permissive additional terms allowed under Section 7 of the GNU
**   Affero GPL version 3 and may not be removed by you.
** 
**   0. Attribution Requirement.
** 
**     You must preserve all legal notices or author attributions in the covered
**     work or Appropriate Legal Notices displayed by works containing the covered
**     work.  You may not remove from the covered work any author or developer
**     credit already included within the covered work.
** 
**   1. No License To Use Trademarks.
** 
**     This license does not grant any license or rights to use the trademarks
**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
**     of Golden Code Development Corporation. You are not authorized to use the
**     name Golden Code, FWD, or the names of any author or contributor, for
**     publicity purposes without written authorization.
** 
**   2. No Misrepresentation of Affiliation.
** 
**     You may not represent yourself as Golden Code Development Corporation or FWD.
** 
**     You may not represent yourself for publicity purposes as associated with
**     Golden Code Development Corporation, FWD, or any author or contributor to
**     the covered work, without written authorization.
** 
**   3. No Misrepresentation of Source or Origin.
** 
**     You may not represent the covered work as solely your work.  All modified
**     versions of the covered work must be marked in a reasonable way to make it
**     clear that the modified work is not originating from Golden Code Development
**     Corporation or FWD.  All modified versions must contain the notices of
**     attribution required in this license.
*/

package com.goldencode.p2j.security;

import java.io.*;
import java.util.*;
import java.security.*;
import java.security.cert.*;
import javax.net.ssl.*;
import com.goldencode.p2j.cfg.*;
import com.goldencode.p2j.directory.Base64;

/**
 * Handles the setup of the JSSE environment to use a custom 
 * <code>KeyManager</code> and <code>TrustManager</code>.  
 * <p>
 * Instances are obtained using getTransportSecurity method of {@link SecurityManager#certificateSm}.
 */
public class TransportSecurity
{
   /** Custom key manager. */
   private KeyManager[] km = null;
   
   /** Custom trust manager. */
   private TrustManager[] tm = null;
   
   /** In-memory trust store. */
   private KeyStore trustStore = null;
   
   /** In-memory key store. */
   private KeyStore keyStore = null;
   
   /**
    * Uses the truststore and keystore resources defined in a given bootstrap
    * configuration to create in-memory <code>KeyStore</code> objects holding
    * certificates (trust store) and the private key (key store). Bootstrap
    * configuration flag <code>net/connection/secure</code> needs to be 
    * enabled for this setup to take place. 
    *  
    * <p>
    * On the client, both the key store and the trust store may be either
    * specified or omitted. The interpretation of these combinations is as
    * follows:
    * <p>
    * <ul>
    *   <li>key store, trust store: fully certified connection where the
    *       client and server are both certified/verified using the given
    *       resources
    *   <li>key store only: client certified, server's certificate is
    *       verified with the J2SE standard trust store
    *   <li>trust store only: anonymous connection, server's certificate
    *       is verified using the specified trust store
    *   <li>no key store, no trust store: anonymous connection, optionally
    *       the server's certificate is verified using the J2SE standard
    *       trust store (if security:certificate:validate=true)
    * </ul>
    * <p>
    * An empty truststore on the server is allowed.  This means no client TLS
    * authentication will be in effect.
    * <p>
    * Key stores, if specified, have to contain one or more key entries.
    * <p>
    * Key entries in use can be specified by processalias or useralias keyword
    * on the client, and alias keyword on the server.
    * <p>
    * Valid combinations on the client are:
    * <ul>
    *   <li>processalias is coded and there is matching alias for a key entry;
    *   <li>useralias is coded and there is matching alias for a key entry;
    *   <li>nothing is coded and there is exactly one key entry
    * </ul>
    * Valid combinations on the server are:
    * <ul>
    *   <li>alias is coded and there is matching alias for a key entry;
    *   <li>nothing is coded and there is exactly one key entry
    * </ul>
    *
    * @param    bc
    *           Security related configuration information.  
    * @param    srv
    *           <code>true</code> if this is the server. 
    * @param    trust
    *           On the server, it is a trust store loaded with certificates, 
    *           built from the directory.  <code>null</code> otherwise. 
    */
   TransportSecurity(BootstrapConfig bc, boolean srv, KeyStore trust)
   throws NoSuchAlgorithmException,
          KeyStoreException,
          UnrecoverableKeyException,
          ConfigurationException
   {
      // bypass all keystore/truststore setup in the insecure case:
      boolean secure = bc.getBoolean("net", "connection", "secure", false);
      if (!secure)
      {
         return;
      }
      
      // obtain the keystore
      keyStore = loadKeyStore(bc, srv);
      boolean loaded = (keyStore != null);
      if (!loaded)
      {
         keyStore = KeyStore.getInstance("JKS");
         
         // no keystore file is specified, the keystore will be empty
         // but only on the client

         if (srv)
         {
            throw new ConfigurationException("misconfigured keystore: file " +
                                             "must be specified");
         }
         
         try
         {
            keyStore.load(null, null);
         }
         
         catch (Exception e)
         {
            // should not be here
         }
      }

      // create a key manager
      KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
      String apw = bc.getString("access", "password", "keyentry", null);
      char[] kspw = (!loaded || apw == null) ? null : apw.toCharArray();
      kmf.init(keyStore, kspw);
      km = kmf.getKeyManagers();

      // create an in-memory truststore
      // On the server, truststore is specified as a parameter.
      // On the client, there are three choices: default J2SE truststore,
      // empty (not specified) trusstore, or with the contens of the specified
      // file. A TrustManager is created for the latter two cases to use this
      // truststore.
      // The default J2SE truststore is used if no truststore was specified
      // and the server's certificate validation is requested.
      
      boolean noTrust = false;

      if (srv)
      {
         // server side truststore initialization
         trustStore = trust;
      }
      else
      {
         // client side truststore initialization (this is a file or URL
         // specified in bootstrap config)
         
         InputStream trustIn = bc.getInputStream("security",
                                                 "truststore", 
                                                 "filename",
                                                 null);
         boolean trustFromFile = true;
         if (trustIn == null)
         {
            String store = bc.getConfigItem("security", "truststore", "bytes");
            if (store != null)
            {
               trustFromFile = false;
               byte[] bytes = Base64.base64ToByteArray(store);
               trustIn = new ByteArrayInputStream(bytes);
            }
         }
         trustStore = KeyStore.getInstance("JKS");

         if (trustIn != null)
         {
            // a truststore file is specified
            try
            {
               String tpw = bc.getString("access",
                                         "password",
                                         "truststore",
                                         null);
               char[] tspw = (tpw == null) ? null : tpw.toCharArray();
               trustStore.load(trustIn, tspw);
               trustIn.close();
            }
            
            catch (Exception e)
            {
               String txt = bc.getString("security",
                                         "truststore", 
                                         (trustFromFile ? "filename" : "bytes"),
                                         null);
               throw new ConfigurationException("misconfigured truststore " +
                                                txt,
                                                e);
            }

            // request server certificate validation 
            bc.setConfigItem("security", "certificate", "validate", "true");
         }
         else
         {
            // no keystore file is specified. the keystore will be empty
            try
            {
               trustStore.load(null, null);
               noTrust = true;
            }
            catch (Exception e)
            {
               // should not be here
            }
         }
      }

      // optionally, create a trust manager for this truststore
      // otherwise, the J2SE default will be used
      if (!noTrust ||
          !bc.getBoolean("security", "certificate", "validate", false))
      {
         tm = getTrustManagers();
      }
      
      // for applet code, to disable server certificate validation it will
      // take a special trust manager that disables checking since there is
      // no local trust store that can be accessed
      String txt = bc.getString("security", "trust_mgr", "disable", "false");
      
      if (txt.equalsIgnoreCase("true"))
      {
          tm = new TrustManager[] { new ZeroActionTrustManager() };
      }
   }

   /**
    * Load the keystore from the specified configuration, and verify if a certificate with the
    * configured alias exists.
    * <p>
    * This loads just the certificate(s), not the private keys.
    * 
    * @param    bc
    *           Security related configuration information.  
    * @param    srv
    *           <code>true</code> if this is the server. 
    *           
    * @return   The loaded key store, or null if it couldn't be loaded.
    */
   static KeyStore loadKeyStore(BootstrapConfig bc, boolean srv)
   throws NoSuchAlgorithmException,
          KeyStoreException,
          UnrecoverableKeyException,
          ConfigurationException
   {
      String alias = getSubjectAlias(bc, srv);

      KeyStore keyStore = null;

      // On the client, the keystore can be empty (not specified) or it can
      // be initialized with the contents of the specified file.  A
      // KeyManager is always created to use this keystore.
      // On the server, an empty keystore is not allowed.

      InputStream keyIn = bc.getInputStream("security", 
                                            "keystore",
                                            "filename",
                                            null);
      boolean keyFromFile = true;
      if (keyIn == null)
      {
         String store = bc.getConfigItem("security", "keystore", "bytes");
         if (store != null)
         {
            keyFromFile = false;
            byte[] bytes = Base64.base64ToByteArray(store);
            keyIn = new ByteArrayInputStream(bytes);
         }
      }
      
      if (keyIn != null)
      {
         // create an in-memory keystore
         keyStore = KeyStore.getInstance("JKS");

         // a keystore file is specified
         try
         {
            String kpw = bc.getString("access", "password", "keystore", null);
            keyStore.load(keyIn, (kpw == null) ? null : kpw.toCharArray());
            keyIn.close();
         }
         
         catch (Exception e)
         {
            String txt = bc.getString("security",
                                      "keystore",
                                      (keyFromFile ? "filename" : "bytes"),
                                      null);
            throw new ConfigurationException("misconfigured keystore " + txt,
                                             e);
         }
         
         // request server certificate validation 
         bc.setConfigItem("security", "certificate", "validate", "true");
   
         // verify the keystore: must have an alias that matches alias
         // or if not specified, exactly one key entry
         alias = verifyKeystore(keyStore, alias);
      }
      
      return keyStore;
   }
   
   /**
    * Initializes the given SSLContext object so that the latter uses the key
    * manager and the trust manager embedded into this TransportSecurity
    * class.
    *
    * @param  sslc
    *         a <code>SSLContext</code> that has to be set up to use the
    *         facilities of this <code>TransportSecurity</code> instance
    * @throws KeyManagementException - if this operation fails
    */
    public void attach(SSLContext sslc)
    throws KeyManagementException 
    {
       sslc.init(km, tm, null);
    }

   /**
    * Returns the trust store.
    *
    * @return trust store built by this <code>TransportSecurity</code>
    */
   KeyStore getTrustStore()
   {
      return trustStore;
   }
    
   /**
    * Looks up the proper subject alias in the given configuration.
    *
    * @param    bc
    *           Security related configuration information.  
    * @param    srv
    *           <code>true</code> if this is the server. 
    *
    * @return   The subject alias or <code>null</code> if no alias is
    *           specified.
    */
   static String getSubjectAlias(BootstrapConfig bc, boolean srv)
   throws ConfigurationException
   {
      String alias;
      if (srv)
      {
         alias = bc.getString("security", "keystore", "alias", null);
      }
      else
      {
         alias = bc.getString("security", "keystore", "processalias", null);
         
         String ua = bc.getString("security", "keystore", "useralias", null);
         
         if (alias != null)
         {
            if (ua != null)
            {
               throw new ConfigurationException("misconfigured keystore: " +
                                                "processalias and useralias" +
                                                " are mutually exclusive");
            }
         }
         else
         {
            alias = ua;
         }
      }
      
      return alias;
   }
   
   /**
    * Verify and normalize the keystore to ensure that the specified alias
    * exists as a key entry OR that there is one and only one key entry.
    * <p>
    * All unnecessary key entries are removed from the key store.
    *
    * @param    keyStore
    *           The key store to verify.
    * @param    alias
    *           The name used to differentiate between multiple key entries.
    *
    * @return   If the <code>alias</code> was specified as <code>null</code>
    *           and the key store only has one key entry, then that name is
    *           returned.  Otherwise the alias passed as a parameter is
    *           returned.
    */
   private static String verifyKeystore(KeyStore keyStore, String alias)
   throws NoSuchAlgorithmException,
          KeyStoreException,
          UnrecoverableKeyException,
          ConfigurationException
   {
      if (alias != null)
      {
         if (!keyStore.isKeyEntry(alias))
         {
            throw new ConfigurationException("misconfigured keystore: " +
                                             "no matching alias for "   +
                                             alias); 
         }
      }
      else
      {
         Enumeration en = keyStore.aliases();
         int numkeys = 0;
         
         while (en.hasMoreElements())
         {
            String next = (String) en.nextElement();
            
            if (keyStore.isKeyEntry(next))
               numkeys ++;
            
            // the first entry is used as the alias
            if (alias == null)
               alias = next;
         }
         
         if (numkeys == 0)
         {
            throw new ConfigurationException("misconfigured keystore: " +
                                             "no private keys");
         }
         
         if (numkeys > 1)
         {
            throw new ConfigurationException("misconfigured keystore: " +
                                             "multiple private keys with " +
                                             "no specified alias to " + 
                                             "differentiate");
         }
      }
      
      Enumeration en = keyStore.aliases();

      // normalize the keystore by deleting all unnecessary entries
      while (en.hasMoreElements())
      {
         String next = (String) en.nextElement();
         
         if (!next.equals(alias))
         {
            keyStore.deleteEntry(next);
         }
      }
      
      return alias;
   }
   
   /**
    * Returns the trust managers which have been initialized using the
    * instance's defined trust store.
    *
    * @return   The default trust manager.
    */
   private TrustManager[] getTrustManagers()
   throws NoSuchAlgorithmException,
          KeyStoreException
   {
      TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
      tmf.init(trustStore);
      return tmf.getTrustManagers();
   }
   
   /**
    * Zero action trust manager class which accepts anything as a valid
    * certificate and produces a zero list of accepted issuers.
    */
   public class ZeroActionTrustManager
   implements X509TrustManager 
   {
      /**
       * Accepts anything.
       */           
      public void checkClientTrusted(X509Certificate[] chain, String authType)
      throws CertificateException 
      {
         // anything is acceptable
      }

      /**
       * Accepts anything.
       */           
      public void checkServerTrusted(X509Certificate[] chain, String authType)
      throws CertificateException 
      {
         // anything is acceptable
      }

      /**
       * Produces zero size list pf accepter issuers.
       *
       * @return  empty array of accepted issuers
       */           
      public X509Certificate[] getAcceptedIssuers() 
      {
         return new X509Certificate[0];
      }
   }
}