package nxm.sys.lib;

import static java.util.Objects.requireNonNull;

import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.Socket;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.net.UnknownHostException;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.KeyStore;
import java.security.KeyStore.CallbackHandlerProtection;
import java.security.KeyStore.LoadStoreParameter;
import java.security.KeyStore.PasswordProtection;
import java.security.KeyStore.PrivateKeyEntry;
import java.security.KeyStore.ProtectionParameter;
import java.security.KeyStoreException;
import java.security.KeyStoreSpi;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.PublicKey;
import java.security.Security;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateFactory;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
import java.security.spec.EncodedKeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64.Decoder;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.Function;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.Manifest;
import java.util.logging.Level;
import java.util.zip.ZipOutputStream;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509ExtendedKeyManager;
import javax.net.ssl.X509KeyManager;
import javax.net.ssl.X509TrustManager;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.x500.X500Principal;
import javax.swing.JOptionPane;

import nxm.sys.inc.InternalUseOnly;
import nxm.sys.inc.MidasReference;
import nxm.sys.inc.ProvisionalUseOnly;
import nxm.sys.lib.FileName.FNCase;

/** Configures Java for using certificate-based authentication. The {@code initSSLContext(..)}
 *  method is intended to be run immediately after startup of the JVM since some of the settings
 *  represent global defaults for the JVM and can not be subsequently altered and/or will have no
 *  effect if set after SSL initialization is initiated (i.e. if an {@link SSLSocket} is opened
 *  before calling {@code initSSLContext(..)} the system defaults will be used by the JVM). <br>
 *  <br>
 *  The name of this class derives from its use with SSL/HTTPS connections and {@link SSLSocket}
 *  even though TLS is current protocol used.<br>
 *  <br>
 *  This class is intended to support the following use-cases:
 *  <pre>
 *    +----------+-----------------------------------------------+----------------+----------------+
 *    | USE CASE |               DESCRIPTION                     | AUTHENTICATION | AUTHORIZATION  |
 *    +----------+-----------------------------------------------+----------------+----------------+
 *    |    #1    | User connects to other resources using their  |      YES       |     N/A        |
 *    |          | personal PKI certificate. User can optionally |  ("user" PKI)  |                |
 *    |          | use a custom trust store.                     |                |                |
 *    +----------+-----------------------------------------------+----------------+----------------+
 *    |    #2    | Service connects to other resources using its |      YES       |     N/A        |
 *    |          | own "server" PKI. Service can optionally use  | ("server" PKI) |                |
 *    |          | a custom trust store.                         |                |                |
 *    |          |                                               |                |                |
 *    |          | This is identical to Use Case #1 except for   |                |                |
 *    |          | the use of a "server" PKI and the mechanisms  |                |                |
 *    |          | used for obtaining PKI password.              |                |                |
 *    +----------+-----------------------------------------------+----------------+----------------+
 *    |    #3    | Service connects to other resources using its |      YES       |     YES        |
 *    |          | own "server" PKI (same as Use Case #2) and    |                |                |
 *    |          | accepts incoming client connections that      |                |                |
 *    |          | require authorization checks be performed     |                |                |
 *    |          | (using an external service) prior to allowing |                |                |
 *    |          | the client to access certain resources.       |                |                |
 *    |          |                                               |                |                |
 *    |          | The client's credentials (distinguished name) |                |                |
 *    |          | can be taken from the SSLSocket used when     |                |                |
 *    |          | they make the incoming connection to the      |                |                |
 *    |          | server's SSLServerSocket.                     |                |                |
 *    |          |                                               |                |                |
 *    |          |    .......................................    |                |                |
 *    |          |    : THIS CAPABILITY IS EXPERIMENTAL!    :    |                |                |
 *    |          |    :.....................................:    |                |                |
 *    |          |                                               |                |                |
 *    |          | Support for authorization is currently        |                |                |
 *    |          | limited and considered experimental.          |                |                |
 *    +----------+-----------------------------------------------+----------------+----------------+
 *  </pre>
 *  The class following configuration options are supported in the properties file (or as parameters
 *  to the {@code initSSLContext(..)} methods:
 *  <pre>
 *                             AUTHENTICATION (CERTIFICATE) PROPERTIES
 *    +-----------------------------------+---------+----------------------------------------------+
 *    |       PROPERTY / [PARAMETER]      | DEFAULT |              DESCRIPTION                     |
 *    +-----------------------------------+---------+----------------------------------------------+
 *    | SSLUtilities.keyStoreFile         |  n/a    | Path to the key store file. Passing a non-   |
 *    | [keyStoreFile]                    |         | null keyStoreFile to the method will override|
 *    |                                   |         | the value in the properties file. Although   |
 *    |                                   |         | this can be a standard Java Key Store (JKS)  |
 *    |                                   |         | file, it is far more common for it to be a   |
 *    |                                   |         | PKCS12 (*.p12 or *.pfx) file, such as those  |
 *    |                                   |         | that can be used with (or exported from)     |
 *    |                                   |         | Firefox or Internet Explorer. This can also  |
 *    |                                   |         | be a PEM file (.pem) with an RSA private key |
 *    |                                   |         | and certificate, such as those typically     |
 *    |                                   |         | used for servers/services and other non-human|
 *    |                                   |         | entities. PEM files with full certificate    |
 *    |                                   |         | chains are currently unsupported.[1]         |
 *    +-----------------------------------+---------+----------------------------------------------+
 *    | SSLUtilities.trustStoreFile       | null    | Path to the trust store file. This usually   |
 *    | [trustPasswdFile]                 |         | be a Java Key Store (JKS) file (if specified)|
 *    |                                   |         | and although it *can* be password-protected, |
 *    |                                   |         | such is exceedingly rare. [1]                |
 *    +-----------------------------------+---------+----------------------------------------------+
 *    | [keyStorePasswd]                  | null    | The key store password. This supports the    |
 *    |                                   |         | following special values:                    |
 *    |                                   |         |   null   - Same as "AUTO".                   |
 *    |                                   |         |   ""     - Same as "AUTO".                   |
 *    |                                   |         |   "AUTO" - Automatically select the proper   |
 *    |                                   |         |            way of obtaining the password     |
 *    |                                   |         |            (usually "GUI"). [2]              |
 *    |                                   |         |   "GUI"  - Present user with a popup asking  |
 *    |                                   |         |            for the password.                 |
 *    |                                   |         |   "NONE" - No password used with the key.    |
 *    |                                   |         | The "NONE" value is present for consistency  |
 *    |                                   |         | with trustStorePasswd, though it should not  |
 *    |                                   |         | be used outside of a testing environment. [3]|
 *    +-----------------------------------+---------+----------------------------------------------+
 *    | [trustStorePasswd]                | null    | The trust store password. This supports the  |
 *    |                                   |         | following special values:                    |
 *    |                                   |         |   null   - Same as "AUTO".                   |
 *    |                                   |         |   ""     - Same as "AUTO".                   |
 *    |                                   |         |   "AUTO" - Automatically select the proper   |
 *    |                                   |         |            way of obtaining the password     |
 *    |                                   |         |            (usually "NONE"). [2]             |
 *    |                                   |         |   "GUI"  - Present user with a popup asking  |
 *    |                                   |         |            for the password.                 |
 *    |                                   |         |   "NONE" - No password used with the key.    |
 *    |                                   |         | The "NONE" value is the most common used with|
 *    |                                   |         | a trust store since it normally contains only|
 *    |                                   |         | public keys which are widely distributed. [3]|
 *    +-----------------------------------+---------+----------------------------------------------+
 *    | SSLUtilities.keyStorePasswdFile   | null    | File containing the key store password. This |
 *    | [keyStorePasswdFile]              |         | is intended for use by service accounts that |
 *    |                                   |         | need to be able to start applications without|
 *    |                                   |         | user intervention (e.g. init.d services).    |
 *    |                                   |         |                                              |
 *    |                                   |         |   .......................................    |
 *    |                                   |         |   : THIS SHOULD NOT BE USED WITH NORMAL :    |
 *    |                                   |         |   : USER ACCOUNTS!                      :    |
 *    |                                   |         |   :.....................................:    |
 *    |                                   |         |                                              |
 *    |                                   |         | The file pointed to will be a text file with |
 *    |                                   |         | the clear-text password on the first line.   |
 *    |                                   |         | Any whitespace will be ignored. The file     |
 *    |                                   |         | should have its file permissions set such    |
 *    |                                   |         | that only the service account has access to  |
 *    |                                   |         | it. [1]                                      |
 *    +-----------------------------------+---------+----------------------------------------------+
 *    | SSLUtilities.capiEnabled          | true    | Attempt to obtain certificates via the       |
 *    |                                   |         | Microsoft Cryptographic Application          |
 *    |                                   |         | Programming Interface (CAPI). This must be   |
 *    |                                   |         | set to "true" or "false". Setting this to    |
 *    |                                   |         | "true" on Linux has no adverse effects (it   |
 *    |                                   |         | acts the same as if run on Windows with no   |
 *    |                                   |         | certificates configured).                    |
 *    +-----------------------------------+---------+----------------------------------------------+
 *    | SSLUtilities.nssEnabled           | true    | Attempt to obtain certificates via the       |
 *    |                                   |         | Firefox Network Security Services (NSS)      |
 *    |                                   |         | interface.  This must be set to "true" or    |
 *    |                                   |         | "false". At this time, this only works on    |
 *    |                                   |         | Linux (i.e. where CAPI is not available).    |
 *    |                                   |         | Setting this to "true" on Windows should     |
 *    |                                   |         | have no adverse effects.                     |
 *    +-----------------------------------+---------+----------------------------------------------+
 *    | SSLUtilities.pksc11Provider       | null    | The provider to use to connect to a PKCS11   |
 *    |                                   |         | provider for token-based authentication. Set |
 *    |                                   |         | this to null to disable use of PKCS11.       |
 *    |                                   |         |                                              |
 *    |                                   |         | This setting is independent of any (PKCS11-  |
 *    |                                   |         | like) use of the Microsoft Cryptographic     |
 *    |                                   |         | Application Programming Interface (CAPI).    |
 *    |                                   |         |                                              |
 *    |                                   |         |   .......................................    |
 *    |                                   |         |   : THIS CAPABILITY IS EXPERIMENTAL!    :    |
 *    |                                   |         |   :.....................................:    |
 *    |                                   |         |                                              |
 *    |                                   |         | Support for PKCS11 is considered experimental|
 *    |                                   |         | at this time due to the variety of PKCS11    |
 *    |                                   |         | providers and a limited ability to test it.  |
 *    +-----------------------------------+---------+----------------------------------------------+
 *    | SSLUtilities.pkcs11Config         | null    | Configuration file used with the PKCS11      |
 *    |                                   |         | provider. This is ignored if PKCS11 support  |
 *    |                                   |         | is disabled (pksc11Provider set to null) or  |
 *    |                                   |         | if the specified provider has already been   |
 *    |                                   |         | configured.                                  |
 *    +-----------------------------------+---------+----------------------------------------------+
 *    | SSLUtilities.sslContextType       |"TLSv1.2"| The SSL context type to use, see SSLContext  |
 *    |                                   |         | for more details. Note that "TLSv1.2" is the |
 *    |                                   |         | only setting that is considered fully-       |
 *    |                                   |         | supported.                                   |
 *    |                                   |         |                                              |
 *    |                                   |         | Do not set this to the default setting (i.e. |
 *    |                                   |         | "Default") as this will cause an exception   |
 *    |                                   |         | to be thrown at the point where the (new) SSL|
 *    |                                   |         | context is initialized using the certificates|
 *    |                                   |         | that have been loaded.                       |
 *    +-----------------------------------+---------+----------------------------------------------+
 *    | SSLUtilities.keyStoreType         | "PKCS12"| The key store type. If this is set to null   |
 *    |                                   |   or    | and the file name of key store used ends with|
 *    |                                   | default | ".p12" or ".pfx" (i.e. a PKCS12 file) the    |
 *    |                                   |         | type "PKCS12" will be used. Otherwise the    |
 *    |                                   |         | normal KeyStore default will be used, see    |
 *    |                                   |         | KeyStore for more details.                   |
 *    +-----------------------------------+---------+----------------------------------------------+
 *    | SSLUtilities.trustStoreType       | default | The trust store type, see KeyStore for more  |
 *    |                                   |         | details.                                     |
 *    +-----------------------------------+---------+----------------------------------------------+
 *    | SSLUtilities.keyMgrAlgorithm      | default | The key manager factory algorithm to use, see|
 *    |                                   |         | KeyManagerFactory for more details.          |
 *    +-----------------------------------+---------+----------------------------------------------+
 *    | SSLUtilities.trustMgrAlgorithm    | default | The trust manager factory algorithm to use,  |
 *    |                                   |         | see TrustManagerFactory for more details     |
 *    +-----------------------------------+---------+----------------------------------------------+
 *    | SSLUtilities.certFilterClass      | null    | The name of a class implementing  CertFilter |
 *    |                                   |         | that will be used to filter the certificates |
 *    |                                   |         | returned. See CertFilter for more details.   |
 *    |                                   |         | If this is null, no filtering will be done.  |
 *    |                                   |         |   .......................................    |
 *    |                                   |         |   : THIS CAPABILITY IS NOT FULLY TESTED :    |
 *    |                                   |         |   :.....................................:    |
 *    +-----------------------------------+---------+----------------------------------------------+
 *    | SSLUtilities.certSelector         | "AUTO"  | Selector to use then multiple matching user  |
 *    |                                   |         | certificates are found (after running any    |
 *    |                                   |         | CertFilter). The valid values are:           |
 *    |                                   |         |   "AUTO"  - Automatically select from one of |
 *    |                                   |         |             the modes below. [2]             |
 *    |                                   |         |   "FIRST" - Always use the first certificate |
 *    |                                   |         |             found. [4]                       |
 *    |                                   |         |   "GUI"   - A graphical popup should prompt  |
 *    |                                   |         |             users.                           |
 *    +-----------------------------------+---------+----------------------------------------------+
 *     Notes: [1] File names take the form of a comma-separated list. The first file found on the
 *                list will be used (this permits paths for both Windows and Linux to appear in
 *                the same file). The file paths can include "${USER}" and "${HOME}" which will
 *                be substituted for the System "user.name" and "user.home" properties respectively.
 *                The paths can also include "${HOST} which will be substituted for the short host
 *                name of the server (e.g. if the host name is "myserver.example.com" then "$HOST"
 *                will become "myserver").
 *            [2] Note that the selections made for "AUTO" modes may change from release to release.
 *            [3] Although the special values (e.g. "AUTO") are technically legal to use as
 *                certificate passwords, they are lousy ones and don't follow any of the recommended
 *                password rules. Users wishing to avoid conflicts between their passwords and the
 *                special values should set a good password.
 *            [4] The "first" certificate found will typically be the first certificate appearing in
 *                the provided PKCS12 file. However, it may differ if certificates are found from
 *                multiple sources (e.g. PKCS11 and PKCS12) or if the underlying PKCS12 provider
 *                (underlying Java API implementation) changes.
 *
 *
 *                                   AUTHROIZATION PROPERTIES
 *    +-----------------------------------+---------+----------------------------------------------+
 *    |       PROPERTY / [PARAMETER]      | DEFAULT |              DESCRIPTION                     |
 *    +-----------------------------------+---------+----------------------------------------------+
 *    | SSLUtilities.userQuery            | null    | REST URL format String to obtain user's      |
 *    |                                   |         | basic information                            |
 *    +-----------------------------------+---------+----------------------------------------------+
 *    | SSLUtilities.groupQuery           | null    | REST URL format String to obtain user's      |
 *    |                                   |         | groups within a specific project             |
 *    +-----------------------------------+---------+----------------------------------------------+
 *  </pre>
 *  Note: In this class, it was necessary to use sun.* package libraries, as Java does not expose
 *  adequate security related API.
 *
 *  @author Jeanette McCall
 *  @author J. Czechowski IV
 *  @since  NeXtMidas 3.7.3 (Note: This class first appeared in the NeXtMidas 3.7.2 release as a
 *                           "hidden" {@link ProvisionalUseOnly} API. However, major changes were
 *                           made before the NeXtMidas 3.7.3 release where it was first considered
 *                           part of the "public" API for NeXtMidas. The class (and methods) are
 *                           generally marked as first appearing in NeXtMidas 3.7.3.)
 */
public class SSLUtilities {
  private SSLUtilities () { } // Prevent instantiation

  private static final int STATE_INIT   =  0; // initSSLContext(..) not called yet
  private static final int STATE_DONE   = +1; // initSSLContext(..) successfully completed
  private static final int STATE_FAILED = -1; // initSSLContext(..) failed to complete

  private static       int                initState    = STATE_INIT;
  private static       String             userQuery    = null;
  private static       String             groupQuery   = null;
  private static       Midas              midas        = null;
  private static       String             userDN       = null;
  private static final List<CertInfo>     allCertInfo  = new ArrayList<>(4);
  private static final Map<String,String> jsonKeyPaths = new LinkedHashMap<>(32);
  private static final Method             PROVIDER_CONFIGURE; // Since 3.9.2 for jdk 11 pkcs11
  /** Certificate property */
  public static enum CertificateProperty {
    /** alias */ ALIAS,
    /** distinguished name */ DN
  }

  /** Logging level for certificate and JSON return information defaulted to
   *  Level.FINEST, but a setter and getter are made for quick adjustment
   *  during debug.
   */
  private static Level certInfoLogLevel= Level.FINEST;

  /** holder of the current keystore in use  @since 3.9.1 */
  private static KeyStore currentKeyStore;
  /** holder of the current truststore in use @since 3.9.1 */
  private static KeyStore currentTrustStore;
  /** Location of nssLibsDir (ex: /usr/lib64) @since 3.9.1 */
  private static String nssLibsDir;
  /** The PCKS11 Provider can only be initialized once, as it as final fields @since 3.9.2 */
  private static boolean pkcs11Initialized;
  /** INSECURE or ANYHOSTNAME or blank for standard server trust mode */
  private static String serverTrustMode = "";
  /** Exception thrown when accessing private members in JDK17+ without jvm option --add-opens */
  private static final String JDK_17_INACCESSIBLEOBJECTEXCEPTION_CLASS_STRING = "class java.lang.reflect.InaccessibleObjectException";
  /** Get the Java major version @since 4.1.4 */
  private static int javaMajVer = Shell.getJavaMajorVersion();

  static {
    // This static block will initialize the method that is only available after java 11.
    // It is used in the tryInitPKCS11 method if it is available. If it isn't, the code
    // goes back to java 8 functionality
    Method m;
    try {
      m = Class.forName("java.security.Provider").getMethod("configure", String.class);
    } catch (Exception e) {
      m = null;
    }
    PROVIDER_CONFIGURE = m;
  }

  /** Configure the SSL context for use. <br>
   *  <br>
   *  This method is identical to {@code initSSLContext(null,null,null,null,null,null,null,null)}.
   *  @return The user's distinguished name (DN) as listed on the certificate.
   *  @throws IllegalStateException If this method is called more than once.
   *  @throws RuntimeException      If initialization fails. Note that if initialization fails the
   *                                JVM may be left in an inconsistent and/or unstable state.
   *  @since NeXtMidas 3.7.3
   */
  public static String initSSLContext () {
    return initSSLContext(null,null,null,null,null,null,null,null);
  }

  /** Configure the SSL context for use. <br>
   *  <br>
   *  This method is identical to {@code initSSLContext(ref,props,null,null,null,null,null,null)}.
   *  @param ref                 Midas reference for status messages, if null any status messages will
   *                             go to {@link System#out}. If this is called as part of startup where
   *                             where the Midas environment is not fully-configured, this must be
   *                             set to null.
   *  @param props               The properties to use when configuring.
   *  @return The user's distinguished name (DN) as listed on the certificate.
   *  @throws IllegalStateException If this method is called more than once.
   *  @throws RuntimeException      If initialization fails. Note that if initialization fails the
   *                                JVM may be left in an inconsistent and/or unstable state.
   *  @since NeXtMidas 3.7.3
   */
  public static String initSSLContext (MidasReference ref, Properties props) {
    return initSSLContext(ref,props,null,null,null,null,null,null);
  }

  /** Configure the SSL context for use. <br>
   *  <br>
   *  This method is identical to {@code initSSLContext(ref,null,propsFileName,null,null,null,null,null)}.
   *  @param ref                 Midas reference for status messages, if null any status messages will
   *                             go to {@link System#out}. If this is called as part of startup where
   *                             where the Midas environment is not fully-configured, this must be
   *                             set to null.
   *  @param propsFileName       The name of the properties file.
   *  @return The user's distinguished name (DN) as listed on the certificate.
   *  @throws IllegalStateException If this method is called more than once.
   *  @throws RuntimeException      If initialization fails. Note that if initialization fails the
   *                                JVM may be left in an inconsistent and/or unstable state.
   *  @since NeXtMidas 3.7.3
   */
  public static String initSSLContext (MidasReference ref, String propsFileName) {
    return initSSLContext(ref,null,propsFileName,null,null,null,null,null);
  }

  /** Configure the SSL context for use. <br>
   *  <br>
   *  Other than {@code ref}, {@code props}, and {@code propsFileName} all of the parameters passed
   *  in serve as overrides to the properties passed in. <br>
   *  <br>
   *  The properties files are read in this manner:
   *  <pre>
   *    +---------------+---------------+----------------------------------------------------------+
   *    |     props     | propsFileName | Properties Read Method                                   |
   *    +---------------+---------------+----------------------------------------------------------+
   *    |     null      |     null      | If the system property "SSLUtilities.properties" is set, |
   *    |               |               | it will be taken as if it is the name of the properties  |
   *    |               |               | file to read in. If the system property is not set, then |
   *    |               |               | the file named "SSLUtilities.properties" will be read in.|
   *    +---------------+---------------+----------------------------------------------------------+
   *    |     null      |   not null    | The name of the properties file to read in will be taken |
   *    |               |               | from the propsFileName parameter.                        |
   *    +---------------+---------------+----------------------------------------------------------+
   *    |   not null    |     null      | The properties passed in will be used as-is.             |
   *    |               |               |                                                          |
   *    +---------------+---------------+----------------------------------------------------------+
   *    |   not null    |   not null    | The properties passed in will be used as-is. The file    |
   *    |               |               | name given will be assumed to match the properties passed|
   *    |               |               | in and used in debug and error messages.                 |
   *    +---------------+---------------+----------------------------------------------------------+
   *  </pre>
   *  @param ref                 Midas reference for status messages, if null any status messages will
   *                             go to {@link System#out}. If this is called as part of startup where
   *                             where the Midas environment is not fully-configured, this must be
   *                             set to null.
   *  @param props               The properties to use when configuring. See note above.
   *  @param propsFileName       The name of the properties file.  See note above.
   *  @param keyStoreFile        Path to user's private key.
   *  @param keyStorePasswd      Password for certificate containing user's private key. This can
   *                             also be set to "AUTO", "GUI", or "NONE".
   *  @param keyStorePasswdFile  The file containing the user's key store password. If specified
   *                             this will override {@code keyStorePasswd}.
   *  @param trustStoreFile      Path to user's trust store.
   *  @param trustStorePasswd    Password for certificate containing the trusted authority (null if
   *                             n/a). Trusted authority certificates are generally public keys and
   *                             have no associated password, so this will usually be null. This can also
   *                             be set to "AUTO", "GUI", or "NONE".
   *  @return The user's distinguished name (DN) as listed on the certificate.
   *  @throws IllegalStateException If this method is called more than once.
   *  @throws RuntimeException      If initialization fails. Note that if initialization fails the
   *                                JVM may be left in an inconsistent and/or unstable state.
   *  @since NeXtMidas 3.7.3
   */
  public static synchronized String initSSLContext (MidasReference ref,    Properties props,
                                                    String propsFileName,  String keyStoreFile,
                                                    String keyStorePasswd, String keyStorePasswdFile,
                                                    String trustStoreFile, String trustStorePasswd) {
    return initSSLContext(ref,props,propsFileName,keyStoreFile,keyStorePasswd,keyStorePasswdFile,trustStoreFile,trustStorePasswd,false);
  }

  /** Same as initSSLContext, except it does not do any host name verification
  *  @param ref                 Midas reference for status messages, if null any status messages will
  *                             go to {@link System#out}. If this is called as part of startup where
  *                             where the Midas environment is not fully-configured, this must be
  *                             set to null.
  *  @param props               The properties to use when configuring. See note above.
  *  @param propsFileName       The name of the properties file.  See note above.
  *  @param keyStoreFile        Path to user's private key.
  *  @param keyStorePasswd      Password for certificate containing user's private key. This can
  *                             also be set to "AUTO", "GUI", or "NONE".
  *  @param keyStorePasswdFile  The file containing the user's key store password. If specified
  *                             this will override {@code keyStorePasswd}.
  *  @param trustStoreFile      Path to user's trust store.
  *  @param trustStorePasswd    Password for certificate containing the trusted authority (null if
  *                             n/a). Trusted authority certificates are generally public keys and
  *                             have no associated password, so this will usually be null. This can also
  *                             be set to "AUTO", "GUI", or "NONE".
  *  @return The user's distinguished name (DN) as listed on the certificate.
  *  @throws IllegalStateException If this method is called more than once.
  *  @throws RuntimeException      If initialization fails. Note that if initialization fails the
  *                                JVM may be left in an inconsistent and/or unstable state.
  *  @since NeXtMidas 3.9.2
  */
  @ProvisionalUseOnly ("API may change, added for evaluation")
  public static synchronized String initAnyHostNameSSL (
                                                   MidasReference ref,    Properties props,
                                                   String propsFileName,  String keyStoreFile,
                                                   String keyStorePasswd, String keyStorePasswdFile,
                                                   String trustStoreFile, String trustStorePasswd) {
   return initSSLContext(ref,props,propsFileName,keyStoreFile,keyStorePasswd,keyStorePasswdFile,trustStoreFile,trustStorePasswd,true);
 }

  /** Configure the SSL context for use. <br>
   *  <br>
  *  @param ref                 Midas reference for status messages, if null any status messages will
  *                             go to {@link System#out}. If this is called as part of startup where
  *                             where the Midas environment is not fully-configured, this must be
  *                             set to null.
  *  @param props               The properties to use when configuring. See note above.
  *  @param propsFileName       The name of the properties file.  See note above.
  *  @param keyStoreFile        Path to user's private key.
  *  @param keyStorePasswd      Password for certificate containing user's private key. This can
  *                             also be set to "AUTO", "GUI", or "NONE".
  *  @param keyStorePasswdFile  The file containing the user's key store password. If specified
  *                             this will override {@code keyStorePasswd}.
  *  @param trustStoreFile      Path to user's trust store.
  *  @param trustStorePasswd    Password for certificate containing the trusted authority (null if
  *                             n/a). Trusted authority certificates are generally public keys and
  *                             have no associated password, so this will usually be null. This can also
  *                             be set to "AUTO", "GUI", or "NONE".
  *  @param noHostNameVerfication should generally be false, but is set to true to turn off host
  *                               name verification (i.e. safer version of basically an insecure mode)
  *  @return The user's distinguished name (DN) as listed on the certificate.
  *  @throws IllegalStateException If this method is called more than once.
  *  @throws RuntimeException      If initialization fails. Note that if initialization fails the
  *                                JVM may be left in an inconsistent and/or unstable state.
  *  @since NeXtMidas 3.7.3 (logic moved to private method in 3.9.2)
  */
  @InternalUseOnly
  public static synchronized String initSSLContext (MidasReference ref,    Properties props,
                                                    String propsFileName,  String keyStoreFile,
                                                    String keyStorePasswd, String keyStorePasswdFile,
                                                    String trustStoreFile, String trustStorePasswd,
                                                    boolean noHostNameVerfication) {
    switch (initState) {
      case STATE_INIT:   break; // OK
      case STATE_DONE: {
        String serverTrustExtention = (serverTrustMode.isEmpty()) ? "" : " with server trust mode '"+serverTrustMode+"'";
        throw new IllegalStateException("Initialization already complete for "+getUserCN()+serverTrustExtention);
      }
      case STATE_FAILED: throw new IllegalStateException("Initialization failed, leaving JVM in an "
                                                        +"inconsistent state, need to restart JVM.");
      default:           throw new AssertionError("Illegal state: "+initState);
    }

    try {
      List<KeyManager>   keyMgrs   = new ArrayList<>(8); // associated with user's private key
      List<TrustManager> trustMgrs = new ArrayList<>(8); // associated with trust store
      List<CertInfo>     certInfo  = new ArrayList<>(8);
      Boolean            useGUI    = null;

      // ==== SET STATIC PROPERTIES ================================================================
      userDN = null;
      midas  = Convert.ref2Midas(ref);

      // ==== LOAD PROPERTIES FILE =================================================================
      props = getProps(props,propsFileName);

      // ==== LOAD AUTHENTICATION (CERTIFICATE) PROPERTIES =========================================
      String  _keyStoreFile        = props.getProperty("SSLUtilities.keyStoreFile");
      String  _trustStoreFile      = props.getProperty("SSLUtilities.trustStoreFile");
      String  _trustStorePasswd    = props.getProperty("SSLUtilities.trustStorePasswd");
      String  _keyStorePasswdFile  = props.getProperty("SSLUtilities.keyStorePasswdFile");
      String  _capiEnabled         = props.getProperty("SSLUtilities.capiEnabled");
      String  _nssEnabled          = props.getProperty("SSLUtilities.nssEnabled");
      String  pksc11Provider       = props.getProperty("SSLUtilities.pksc11Provider");
      String  pkcs11Config         = props.getProperty("SSLUtilities.pkcs11Config");
      String  sslContextType       = props.getProperty("SSLUtilities.sslContextType");
      String  keyStoreType         = props.getProperty("SSLUtilities.keyStoreType");
      String  trustStoreType       = props.getProperty("SSLUtilities.trustStoreType");
      String  keyMgrAlgorithm      = props.getProperty("SSLUtilities.keyMgrAlgorithm");
      String  trustMgrAlgorithm    = props.getProperty("SSLUtilities.trustMgrAlgorithm");
      String  certFilterClass      = props.getProperty("SSLUtilities.certFilterClass");
      String  certSelector         = props.getProperty("SSLUtilities.certSelector");
      boolean capiEnabled;
      boolean nssEnabled;

      keyStoreFile       = applyDefault(keyStoreFile,      findFile(_keyStoreFile));
      trustStoreFile     = applyDefault(trustStoreFile,    findFile(_trustStoreFile));
      keyStorePasswdFile = applyDefault(keyStorePasswdFile,findFile(_keyStorePasswdFile));
      keyStorePasswd     = applyDefault(keyStorePasswd,    "AUTO");
      trustStorePasswd   = applyDefault(trustStorePasswd,  applyDefault(_trustStorePasswd, "AUTO"));
      capiEnabled        = applyDefault(_capiEnabled,      true);
      nssEnabled         = applyDefault(_nssEnabled,       true);
      pksc11Provider     = applyDefault(pksc11Provider,    null);
      sslContextType     = applyDefault(sslContextType,    "TLSv1.2");
      trustStoreType     = applyDefault(trustStoreType,    KeyStore.getDefaultType());
      keyMgrAlgorithm    = applyDefault(keyMgrAlgorithm,   KeyManagerFactory.getDefaultAlgorithm());
      trustMgrAlgorithm  = applyDefault(trustMgrAlgorithm, TrustManagerFactory.getDefaultAlgorithm());
      certSelector       = applyDefault(certSelector,      "AUTO");
      pkcs11Config       = findFile(pkcs11Config);
      trustStoreFile     = findFile(trustStoreFile);
      keyStorePasswdFile = findFile(keyStorePasswdFile);

      // ==== LOAD AUTHORIZATION PROPERTIES ========================================================
      userQuery                   = props.getProperty("SSLUtilities.userQuery");
      groupQuery                  = props.getProperty("SSLUtilities.groupQuery");

      Enumeration<?> propNames = props.propertyNames();
      jsonKeyPaths.clear(); // clear, in case previously loaded via override
      while (propNames.hasMoreElements()) {
        String key = propNames.nextElement().toString();
        String val = props.getProperty(key);

        if (key.startsWith("SSLUtilities.responsePath.")) {
          String jsonKey = key.replaceFirst("SSLUtilities.responsePath.","");
          jsonKeyPaths.put(jsonKey.toLowerCase(), val);
        }
      }

      String[] loggingValues = {propsFileName,keyStoreFile,trustStoreFile,keyStorePasswd,
                trustStorePasswd,keyStorePasswdFile,String.valueOf(capiEnabled),String.valueOf(nssEnabled),
                pkcs11Config,pksc11Provider,sslContextType,keyStoreType,trustStoreType,keyMgrAlgorithm,
                trustMgrAlgorithm,certFilterClass,certSelector};
      LogUtilities.log(SSLUtilities.class, midas, certInfoLogLevel,
          () -> logInitializationProperties (loggingValues[0], loggingValues[1], loggingValues[2],
              loggingValues[3], loggingValues[4], loggingValues[5], loggingValues[6], loggingValues[7],
              loggingValues[8], loggingValues[9], loggingValues[10], loggingValues[11],loggingValues[12],
              loggingValues[13],loggingValues[14], loggingValues[15],loggingValues[16]));


      // ==== TRY AUTOMATIC STUFF ==================================================================
      // Only one PCKS11 Provider can be created and some of its fields are final.
      // If pksc11Provider and pkcs11Config are defined, they will be used
      // If capiEnabled is true and on a Windows OS, then CAPI will be tried
      // Otherwise, NSS will be tried.
      // Note: each of these methods will return immediately if the proper conditions are not met
      tryInitPKCS11(certInfo, keyMgrs, trustMgrs, pksc11Provider, pkcs11Config);
      tryInitCAPI(  certInfo, keyMgrs, trustMgrs, capiEnabled);
      tryInitNSS(   certInfo, keyMgrs, trustMgrs, nssEnabled, !StringUtil.isEmpty(nssLibsDir));

      // ==== LOAD CERTS FROM FILE =================================================================
      try {
        // ==== LOAD PRIVATE KEY ===================================================================
        loadKeyStore(certInfo, keyMgrs, capiEnabled,nssEnabled, pksc11Provider,
            pkcs11Config, keyStoreFile, keyMgrAlgorithm, keyStoreType,certFilterClass, keyStorePasswd,
            keyStorePasswdFile, certSelector, useGUI);

        // ==== LOAD TRUST STORE ===================================================================
        if (trustStoreFile != null) {
          try (BaseFile truststoreFile = new BaseFile(midas,trustStoreFile)) {
            InputStream inStreamTrust = truststoreFile.findResource(IOResource.DIR_INPUT).getInputStream();

            // ==== LOAD THE TRUST STORE ===========================================================
            char[] trustPasswdChars = getTrustStorePasswd(trustStoreFile, trustStorePasswd, null);
            KeyStore ts = KeyStore.getInstance(trustStoreType);
            ts.load(null);
            ts.load(inStreamTrust, trustPasswdChars);

            // ==== GET THE TRUST MANAGERS =========================================================
            TrustManagerFactory tmf = TrustManagerFactory.getInstance(trustMgrAlgorithm);
            tmf.init(ts);
            currentTrustStore = ts;
            trustMgrs.addAll(0, Arrays.asList(tmf.getTrustManagers())); // Add it to the front, it will not work unless it is in front 3.9.1
          }
        }

        // ==== INIT THE SSL CONTEXT ===============================================================
        SSLContext     sslContext = SSLContext.getInstance(sslContextType);
        KeyManager[]   _keyMgrs   = keyMgrs.toArray(new KeyManager[keyMgrs.size()]);
        TrustManager[] _trustMgrs = trustMgrs.toArray(new TrustManager[trustMgrs.size()]);

        sslContext.init(_keyMgrs, _trustMgrs, null);

        SSLSocketFactory socketFactory = sslContext.getSocketFactory();

        SSLContext.setDefault(sslContext);
        HttpsURLConnection.setDefaultSSLSocketFactory(socketFactory);
        if (noHostNameVerfication) HttpsURLConnection.setDefaultHostnameVerifier(new AcceptAllHostNameVerifier()); // NXM-3458

        if (true) {
          // When running under Java Web Start, Web Start will substitute it's own custom handlers
          // and ignore the socket factory we just set. I haven't figured out where in the code to
          // tell the networking libraries to ignore the Web Start settings and use what we just set.
          // In theory, unsetting the "java.protocol.handler.pkgs" System property will accomplish
          // this; however, that does not appear to work in most situations (at least not on Java 8).
          //
          // I found empirically (under Java 8) that if you force the loading by "opening" an
          // HTTPS connection (which sets stuff up, but doesn't actually connect) you can then
          // set the socket factory a second time and it will persist. This isn't ideal, but
          // works for now...
          (new URI("https://example.com")).toURL().openConnection(); // <-- This looks wrong, but is correct!
          HttpsURLConnection.setDefaultSSLSocketFactory(socketFactory);
          if (noHostNameVerfication) HttpsURLConnection.setDefaultHostnameVerifier(new AcceptAllHostNameVerifier()); // NXM-3458 - not sure we need to repeat this too
        }

        SSLContext defaultSSLContext = SSLContext.getDefault();
        LogUtilities.log(SSLUtilities.class, midas, certInfoLogLevel,
          () -> logSSLContext(Arrays.toString(_keyMgrs),Arrays.toString(_trustMgrs),
              sslContext,_keyMgrs,_trustMgrs,socketFactory,defaultSSLContext));
      }
      catch (Exception ex) {
        throw new MidasException("SSL init failed", ex);
      }

      // ==== DONE =================================================================================
      initState = STATE_DONE;
      if (noHostNameVerfication) serverTrustMode = "ANYHOSTNAME";
      return getUserDN();
    }
    catch (RuntimeException|Error e) {
      initState = STATE_FAILED;
      throw e;
    }
  }

  /** Get properties from Properties file
   *
   *  @param props          Existing Properties, if availabble
   *  @param propsFileName  Name of Properties file
   *  @return Properties
   */
  @InternalUseOnly
  public static Properties getProps (Properties props, String propsFileName) {

    if (props != null) {
      if (propsFileName == null) {
        propsFileName = "<props>";
      }
    }
    else {
      if ((propsFileName == null) || propsFileName.isEmpty()) {
        propsFileName = System.getProperty("SSLUtilities.properties");
      }
      if ((propsFileName == null) || propsFileName.isEmpty()) {
        propsFileName = "SSLUtilities.properties";
      }

      // ---- Try the Midas path (includes local disk) -----------------------------------------
      props = loadPropsFile(propsFileName, true);

      // ---- Try the Java classpath -----------------------------------------------------------
      if (props == null) {
        ClassLoader cl = SSLUtilities.class.getClassLoader();
        URL propFileUrl = cl.getResource(propsFileName);
        if (propFileUrl == null) {
          String auxes = Shell.getMidasContext().results.getTable("AUX").toString();
          throw new MidasException("Properties file '" + propsFileName +
              "' not found in classpath or Midas path (current aux list:"+auxes+")");
        }
        propsFileName = propFileUrl.toString();
        props         = loadPropsFile(propsFileName, false);
      }
    }
    return props;
  }

  /** Load Key Store
   *  @param certInfo           (I/O) The certificates found
   *  @param keyMgrs            Key managers to use
   *  @param capiEnabled        Attempt to obtain certificates via CAPI?
   *  @param nssEnabled         Attempt to obtain certificates via NSS?
   *  @param pksc11Provider     Used to connect to a PKCS11 provider for token-based authentication
   *  @param pkcs11Config       Configuration file used with the PKCS11 provider
   *  @param keyStoreFile       KeyStore file hold user cert
   *  @param keyMgrAlgorithm    KeyStore Manager Algorithm
   *  @param keyStoreType       KeyStore type
   *  @param certFilterClass    Class implementing  CertFilter used to filter the certificates
   *  @param keyStorePasswd     KeyStore password
   *  @param keyStorePasswdFile File containing KeyStore password
   *  @param certSelector       Selector to use then multiple matching user certificates
   *  @param useGUI             Use GUI prompt to select bewteen multiple certificates
   *  @return User's Distinguished Name
   *  @throws Exception throws MidasException and can throw others
   */
  @SuppressWarnings("fallthrough")
  @InternalUseOnly
  public static String loadKeyStore (List<CertInfo> certInfo, List<KeyManager> keyMgrs,
      boolean capiEnabled, boolean nssEnabled,
      String pksc11Provider, String pkcs11Config, String keyStoreFile,
      String keyMgrAlgorithm, String keyStoreType, String certFilterClass, String keyStorePasswd,
      String keyStorePasswdFile, String certSelector, Boolean useGUI)
      throws Exception {

    KeyStore ks;
    boolean isWindows = Shell.isWindows();
    // ==== LOAD PRIVATE KEY ===================================================================
    String keyStoreExtension = new FileName(keyStoreFile).getExt();
    if ((keyStoreFile == null) || !certInfo.isEmpty()) {
      // skip this step
      ks = null;
    }
    else if (keyStoreExtension.equalsIgnoreCase("pem")) {
      ks = readPemFile(keyStoreFile, null).getKeyStore(); // NXM-4104 Handle PEM chain and prefer first certificate
      KeyManagerFactory kmf = KeyManagerFactory.getInstance(keyMgrAlgorithm);
      kmf.init(ks, null);
      findCertificates(certInfo, kmf, ks, null, capiEnabled & isWindows);
      if (certInfo.isEmpty()) {
        LogUtilities.log(SSLUtilities.class, midas, Level.WARNING,
            "First PEM cert chain was not valid, switching to code that uses last valid cert and does not load full chain");
        // try with legacy version that prefers last certificate
        ks = readPemFilePreferLast(keyStoreFile, null);
        kmf.init(ks, null);
        findCertificates(certInfo, kmf, ks, null, capiEnabled & isWindows);
      }
    }
    else {
      try (BaseFile privKeyFile = new BaseFile(midas,keyStoreFile)) {
        InputStream inStreamKey = privKeyFile.findResource(IOResource.DIR_INPUT).getInputStream();

        // ==== LOAD THE KEY STORE / KEY MANAGER (PART 1) ======================================
        if ((keyStoreType == null) || keyStoreType.isEmpty()) {
          if (keyStoreExtension.equalsIgnoreCase("p12") ||
              keyStoreExtension.equalsIgnoreCase("pfx")) {
            // Files with the ".p12" (from Firefox, et al.) or ".pfx" (from Windows, et al.) are
            // PKCS12 files and should load as such. If this is not used, it is likely that the
            // default JKS type will be used and the PKCS12 nested a few levels deep under a
            // delegate (which makes later stuff more annoying).
            keyStoreType = "PKCS12";
          }
          else if (keyStoreExtension.equalsIgnoreCase("jks")) {
            keyStoreType = "JKS";
          }
          else {
            // WARNING: The default type is expected to change from "JKS"
            // under Java 8 (and earlier) to "PKCS12" under Java 9.
            keyStoreType = KeyStore.getDefaultType();
          }
        }

        ks = KeyStore.getInstance(keyStoreType);
        KeyManagerFactory kmf = KeyManagerFactory.getInstance(keyMgrAlgorithm); // fixed in 3.9.2

        // ==== WORK-AROUND FOR DUPLICATES IN PKCS12 FILE ======================================
        // Users with multiple certificates within a PKCS12 ("*.p12") file will frequently have
        // the certificates listed with the same alias (e.g. "Digital Signature") as this is the
        // default PKCS12 file-generation behavior for a number of tools (including the export
        // from Firefox).
        //
        // Java fails to consider this and when reading in the file will dump all of the keys
        // into a Map that maps alias to key. As such, if multiple keys have the same alias,
        // only one of them is retained.
        //
        // This work-around is a "hack" that swaps in a modified version of Map (HistoryMap) for
        // the normal Map that is internal to the PKCS12KeyStore. The acts as normal Map, except
        // that it also keeps a history of all entries inserted, allowing the "duplicate" entries
        // to be pulled out later.
        try {
          KeyStoreSpi keyStoreSpi = getKeyStoreSpi(ks);
          if (keyStoreSpi.getClass().getName().equals("sun.security.pkcs12.PKCS12KeyStore")) {
            // The type on 'entries' varies slightly with the different Java versions...
            //   Java 7: Hashtable<String,PKCS12KeyStore.KeyEntry>
            //   Java 8: Map<String,PKCS12KeyStore.KeyEntry>
            Field entries   = keyStoreSpi.getClass().getDeclaredField("entries");
            Field modifiers = Field.class.getDeclaredField("modifiers");
            modifiers.setAccessible(true);
            modifiers.set(entries, entries.getModifiers() & ~Modifier.FINAL);
            entries.setAccessible(true);
            entries.set(keyStoreSpi, new EntriesMap<>());
          }
        }
        catch (ReflectiveOperationException e) {
          throw new MidasException("Unable to configure PKCS12 KeyStoreSpi", e);
        }
        catch (RuntimeException e) {
          if (e.getClass().toString().equals(JDK_17_INACCESSIBLEOBJECTEXCEPTION_CLASS_STRING)) {
            LogUtilities.log(SSLUtilities.class, midas, Level.WARNING,
                "findCertificates unable access private fields to work-around duplicates in PKCS12 certificates add this jvm option --add-opens java.base/java.security=ALL-UNNAMED",e);
          }
          else throw e;
        }

        // ==== LOAD THE KEY STORE / KEY MANAGER (PART 2) ======================================
        char[] keyPasswdChars = getKeyStorePasswd(keyStoreFile, keyStorePasswd, keyStorePasswdFile);
        ks.load(inStreamKey, keyPasswdChars);
        kmf.init(ks, keyPasswdChars);

        findCertificates(certInfo, kmf, ks, keyPasswdChars,capiEnabled & isWindows);
      }
    }
    if (ks != null) currentKeyStore = ks;
    // ==== SELECT THE BEST CERTIFICATE ========================================================
    if ((certFilterClass != null) && !certFilterClass.isEmpty()) {
      CertFilter certFilter = (CertFilter)Class.forName(certFilterClass).getConstructor().newInstance(); // Updated in 3.9.0 to use default constructor
      certInfo = certFilter.filter(certInfo);
    }

    if (certInfo.size() > 1) {
      switch (certSelector) {
        default:
          throw new IllegalArgumentException("Invalid value certSelector='"+certSelector+"'");
        case "FIRST":
          certInfo = Collections.singletonList(certInfo.get(0));
          break;
        case "AUTO":
          if ((useGUI != null) && !useGUI) {
            LogUtilities.log(SSLUtilities.class, certInfoLogLevel, "The first certificate is being selected");
            certInfo = Collections.singletonList(certInfo.get(0));
            break;
          }
          // FALLTHROUGH
        case "GUI":
          int      messageType = JOptionPane.QUESTION_MESSAGE;
          String   title       = "NeXtMidas SSL Initialization";
          String   message     = "Select certificate to use";
          Object[] choices     = certInfo.toArray(new CertInfo[certInfo.size()]);
          Object choice        = Shell.createPopupWithSelections(messageType, title, message, choices); // Changed to use new graphical popup in Shell.java since 3.9.2 for Java 11 modularity

          if (choice == null) {
            certInfo = Collections.singletonList(certInfo.get(0));  // let it default to the first one
          }
          else {
            certInfo = Collections.singletonList((CertInfo)choice); // take the user's choice
          }
        break;
      }
    }

    // ==== USE THE BEST CERTIFICATE ===========================================================
    if (!certInfo.isEmpty()) {
      keyMgrs.add(new CertKeyManager(certInfo.get(0).kmf, certInfo));
      allCertInfo.addAll(certInfo);
      userDN = certInfo.get(0).getUserDN();
    }
    return userDN;
  }

  /** Initialize an Insecure SSL session (AKA trust everything)
   *  @param ref                  Midas Reference
   *  @param keyStoreFile         KeyStore file
   *  @param keyStorePasswd       Keystore password
   *  @param keyStorePasswdFile   KeyStore password file
   *  @return User distinguished name
   *  @since NeXtMidas 3.9.0
   */
  @ProvisionalUseOnly("WARNING: This is dangerous to use!")
  public static synchronized String initInsecureSSL (MidasReference ref, String keyStoreFile,
      String keyStorePasswd, String keyStorePasswdFile) {
    return initInsecureSSL(ref, keyStoreFile, keyStorePasswd, keyStorePasswdFile, null, false);
  }

  /** Initialize an Insecure SSL session (AKA trust everything)
   *  @param ref                  Midas Reference
   *  @param keyStoreFile         KeyStore file
   *  @param keyStorePasswd       Keystore password
   *  @param keyStorePasswdFile   KeyStore password file
   *  @param propsFileName        The name of the properties file.
   *  @param userFromProps        Try to get user certificate and password from properties file
   *  @return User distinguished name
   *  @since NeXtMidas 4.0.0
   */
  @ProvisionalUseOnly("WARNING: This is dangerous to use!")
  public static synchronized String initInsecureSSL (MidasReference ref, String keyStoreFile,
      String keyStorePasswd, String keyStorePasswdFile, String propsFileName, boolean userFromProps) {

    switch (initState) {
      case STATE_INIT:   break; // OK
      case STATE_DONE: {
        String serverTrustExtention = (serverTrustMode.isEmpty()) ? "" : " with server trust mode '"+serverTrustMode+"'";
        throw new IllegalStateException("Initialization already complete for "+getUserCN()+serverTrustExtention);
      }
      case STATE_FAILED: throw new IllegalStateException("Initialization failed, leaving JVM in an "
                                                        +"inconsistent state, need to restart JVM.");
      default:           throw new AssertionError("Illegal state: "+initState);
    }

    try {
      List<KeyManager>   keyMgrs   = new ArrayList<>(8); // associated with user's private key
      List<TrustManager> trustMgrs = new ArrayList<>(8); // associated with trust store
      List<CertInfo>     certInfo  = new ArrayList<>(8);
      Boolean            useGUI    = null;

      // Since the insecure option does not generally use a properties file, use default values
      boolean capiEnabled       = !userFromProps;
      boolean nssEnabled        = !userFromProps;
      String  keyMgrAlgorithm   = KeyManagerFactory.getDefaultAlgorithm();
      String  keyStoreType      = null;
      String  certFilterClass   = null;
      String  certSelector      = "AUTO";

      // only use properties file for user, if that option is set
      if (userFromProps) {
        Properties props = getProps(null, propsFileName);
        // ==== LOAD AUTHENTICATION (CERTIFICATE) PROPERTIES =========================================
        String  _keyStoreFile        = props.getProperty("SSLUtilities.keyStoreFile");
        String  _keyStorePasswdFile  = props.getProperty("SSLUtilities.keyStorePasswdFile");
        keyStoreFile       = applyDefault(keyStoreFile,      findFile(_keyStoreFile));
        keyStorePasswdFile = applyDefault(keyStorePasswdFile,findFile(_keyStorePasswdFile));
        String  _capiEnabled         = props.getProperty("SSLUtilities.capiEnabled");
        String  _nssEnabled          = props.getProperty("SSLUtilities.nssEnabled");
        capiEnabled        = applyDefault(_capiEnabled, false);
        nssEnabled         = applyDefault(_nssEnabled, false);
        // If available these properties will be retrieved
        userQuery                   = props.getProperty("SSLUtilities.userQuery");
        groupQuery                  = props.getProperty("SSLUtilities.groupQuery");
        Enumeration<?> propNames = props.propertyNames();
        jsonKeyPaths.clear(); // clear in case previously loaded via override
        while (propNames.hasMoreElements()) {
          String key = propNames.nextElement().toString();
          String val = props.getProperty(key);

          if (key.startsWith("SSLUtilities.responsePath.")) {
            String jsonKey = key.replaceFirst("SSLUtilities.responsePath.","");
            jsonKeyPaths.put(jsonKey.toLowerCase(), val);
          }
        }
      }

      if (keyStorePasswd == null) keyStorePasswd = "AUTO";

      // ==== SET STATIC PROPERTIES ================================================================
      userDN = null;
      midas  = Convert.ref2Midas(ref);

      // ==== TRY AUTOMATIC STUFF ==================================================================
      // Only one PCKS11 Provider can be created and some of its fields are final.
      // If capiEnabled is true and on a Windows OS, then CAPI will be tried
      // Otherwise, NSS will be tried.
      // Note: each of these methods will return immediately if the proper conditions are not met
      tryInitCAPI(certInfo, keyMgrs, trustMgrs, capiEnabled);
      tryInitNSS( certInfo, keyMgrs, trustMgrs, nssEnabled, !StringUtil.isEmpty(nssLibsDir));

      try {
        // ==== LOAD PRIVATE KEY ===================================================================
        loadKeyStore(certInfo, keyMgrs, capiEnabled,nssEnabled, null,
            null, keyStoreFile, keyMgrAlgorithm, keyStoreType,certFilterClass, keyStorePasswd,
            keyStorePasswdFile, certSelector, useGUI);

        // Load all trusting trust store
        TrustManager[] trustAllCerts = getAllTrustingTrustManager ();

        SSLContext sslContext;
        sslContext = SSLContext.getInstance("TLSv1.2");
        KeyManager[]   _keyMgrs   = keyMgrs.toArray(new KeyManager[keyMgrs.size()]);
        sslContext.init(_keyMgrs, trustAllCerts, null);
        HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());
        HttpsURLConnection.setDefaultHostnameVerifier(new AcceptAllHostNameVerifier());
        SSLSocketFactory socketFactory = sslContext.getSocketFactory();
        SSLContext.setDefault(sslContext);
        HttpsURLConnection.setDefaultSSLSocketFactory(socketFactory);

        if (true) {
          // When running under Java Web Start, Web Start will substitute it's own custom handlers
          // and ignore the socket factory we just set. I haven't figured out where in the code to
          // tell the networking libraries to ignore the Web Start settings and use what we just set.
          // In theory, unsetting the "java.protocol.handler.pkgs" System property will accomplish
          // this; however, that does not appear to work in most situations (at least not on Java 8).
          //
          // I found empirically (under Java 8) that if you force the loading by "opening" an
          // HTTPS connection (which sets stuff up, but doesn't actually connect) you can then
          // set the socket factory a second time and it will persist. This isn't ideal, but
          // works for now...
          (new URI("https://example.com")).toURL().openConnection(); // <-- This looks wrong, but is correct!
          HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());
        }

      }
      catch (Exception ex) {
        throw new MidasException("SSL init failed", ex);
      }

        // ==== DONE =================================================================================
        initState = STATE_DONE;
        serverTrustMode = "INSECURE";
        return getUserDN();
      }
      catch (RuntimeException|Error e) {
        initState = STATE_FAILED;
        throw e;
      }
  }

  private static TrustManager[] getAllTrustingTrustManager () {
    // Load all trusting trust store
    TrustManager[] trustAllCerts = new TrustManager[]{
        new javax.net.ssl.X509ExtendedTrustManager() {

          @Override
          public X509Certificate[] getAcceptedIssuers () {
            return null;
          }

          @Override
          public void checkServerTrusted (X509Certificate[] chain, String authType) throws CertificateException {
          }

          @Override
          public void checkClientTrusted (X509Certificate[] chain, String authType) throws CertificateException {
          }

          @Override
          public void checkServerTrusted (X509Certificate[] chain, String authType, SSLEngine engine)
              throws CertificateException {
          }

          @Override
          public void checkServerTrusted (X509Certificate[] chain, String authType, Socket socket) throws CertificateException {
          }

          @Override
          public void checkClientTrusted (X509Certificate[] chain, String authType, SSLEngine engine)
              throws CertificateException {
          }

          @Override
          public void checkClientTrusted (X509Certificate[] chain, String authType, Socket socket) throws CertificateException {

          }
        }
    };
    return trustAllCerts;
  }
  /** Loads the properties file.
   *  @param propsFileName The file name.
   *  @param optional      Is the file optional?
   *  @return The properties or null if optional and not found.
   */
  @InternalUseOnly
  public static Properties loadPropsFile (Object propsFileName, boolean optional) {
    Properties props = null;
    int        flags = (optional)? BaseFile.INPUT|BaseFile.OPTIONAL|BaseFile.NOABORT
                                 : BaseFile.INPUT;

    try (BaseFile propFile = new BaseFile(midas,propsFileName)) {
      if (!propFile.exists() && !optional) {
        throw new MidasException("Properties file not found: "+propFile);
      }
      if (propFile.open(flags)) {
        try (InputStream in = propFile.getResource().getInputStream()) {
          props = new Properties();
          props.load(in);
        }
        catch (IOException e) {
          throw new MidasException("Error loading "+propsFileName, e);
        }
        // Make sure everything is valid (the properties load does a lousy job of error
        // checking and will even accept a binary (non-properties) file without error.
        // So we make sure that at least one of our keys is set in the file.
        Enumeration<?> names = props.propertyNames();
        boolean        ok    = false;
        while (!ok && names.hasMoreElements()) {
          ok = names.nextElement().toString().startsWith("SSLUtilities.");
        }
        if (!ok) {
          throw new MidasException("Error loading "+propsFileName+": File does not contain "+
                                   "any of the required SSLUtilities.* keys.");
        }
        return props;
      }
    }
    return null;
  }

  private static String logInitializationProperties (String propsFileName, String keyStoreFile,
      String trustStoreFile, String keyStorePasswd, String trustStorePasswd, String keyStorePasswdFile,
      String capiEnabled, String nssEnabled, String pkcs11Config, String pksc11Provider,
      String sslContextType, String keyStoreType, String trustStoreType, String keyMgrAlgorithm,
      String trustMgrAlgorithm, String certFilterClass, String certSelector) {
    StringBuilder debugStr = new StringBuilder(32768);
    debugStr.append("\n+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n");
    debugStr.append("Java System Properties:").append(toStr(System.getProperties())   ).append('\n');
    debugStr.append("+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n");
    debugStr.append("SSLUtilities.initSSLContext(..) initializing with:\n");
    debugStr.append("  props              = ").append(propsFileName                   ).append('\n');
    debugStr.append("  keyStoreFile       = ").append(keyStoreFile                    ).append('\n');
    debugStr.append("  trustStoreFile     = ").append(trustStoreFile                  ).append('\n');
    debugStr.append("  keyStorePasswd     = ").append(passwdToStr(keyStorePasswd)     ).append('\n');
    debugStr.append("  trustStorePasswd   = ").append(passwdToStr(trustStorePasswd)   ).append('\n');
    debugStr.append("  keyStorePasswdFile = ").append(keyStorePasswdFile              ).append('\n');
    debugStr.append("  capiEnabled        = ").append(capiEnabled                     ).append('\n');
    debugStr.append("  nssEnabled         = ").append(nssEnabled                      ).append('\n');
    debugStr.append("  pkcs11Config       = ").append(pkcs11Config                    ).append('\n');
    debugStr.append("  pksc11Provider     = ").append(pksc11Provider                  ).append('\n');
    debugStr.append("  sslContextType     = ").append(sslContextType                  ).append('\n');
    debugStr.append("  keyStoreType       = ").append(keyStoreType                    ).append('\n');
    debugStr.append("  trustStoreType     = ").append(trustStoreType                  ).append('\n');
    debugStr.append("  keyMgrAlgorithm    = ").append(keyMgrAlgorithm                 ).append('\n');
    debugStr.append("  trustMgrAlgorithm  = ").append(trustMgrAlgorithm               ).append('\n');
    debugStr.append("  certFilterClass    = ").append(certFilterClass                 ).append('\n');
    debugStr.append("  certSelector       = ").append(certSelector                    ).append('\n');
    debugStr.append("  -------------------------------------------------------------------------\n");
    debugStr.append("  userQuery          = ").append(userQuery                       ).append('\n');
    debugStr.append("  groupQuery         = ").append(groupQuery                      ).append('\n');
    debugStr.append("+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n");
    return debugStr.toString();
  }

  private static String logSSLContext (String keyMgrs, String trustMgrs, SSLContext ctx, KeyManager[] _keyMgrs,
      TrustManager[] _trustMgrs, SSLSocketFactory socketFactory, SSLContext defaultSSLContext) {
    StringBuilder debugStr = new StringBuilder(32768);
    debugStr.append("\n+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n");
    debugStr.append("SSLUtilities.initSSLContext(..) setting SSL context to use:\n");
    debugStr.append("  _keyMgrs   = ").append(toStr(Arrays.toString(_keyMgrs)  )).append('\n');
    debugStr.append("  _trustMgrs = ").append(toStr(Arrays.toString(_trustMgrs))).append('\n');
    debugStr.append("  ctx                                   = ").append(
              toStr(ctx.getProtocol()                          )).append('\n');
    debugStr.append("  socketFactory default cipher suites   = ").append(
              toStr(socketFactory.getDefaultCipherSuites()     )).append('\n');
    debugStr.append("  socketFactory supported cipher suites = ").append(
              toStr(socketFactory.getSupportedCipherSuites()   )).append('\n');
    debugStr.append("  SSLContext.getDefault()               = ").append(
              toStr(defaultSSLContext.getProtocol())      ).append('\n');
    debugStr.append("+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n");
    return debugStr.toString();
  }

  /** Applies a default value.
   *  @param value The current value.
   *  @param def   The default to apply if the value is null or the empty string.
   *  @return The value to use.
   */
  private static boolean applyDefault (String value, boolean def) {
    if (value == null        ) return def;
    if (value.isEmpty()      ) return def;
    if (value.equals("true" )) return true;
    if (value.equals("false")) return false;
    throw new IllegalArgumentException("Expected 'true' or 'false' but given '"+value+"'");
  }

  /** Applies a default value.
   *  @param value The current value.
   *  @param def   The default to apply if the value is null or the empty string.
   *  @return The value to use.
   */
  private static String applyDefault (String value, String def) {
    if (value == null  ) return def;
    if (value.isEmpty()) return def;
    return value;
  }

  /** Converts a password to a printable string that can be used in log messages. Any special
   *  values will be printed as-is while everything else will just become "********".
   *  @param passwd The password.
   *  @return The version that can be printed to the log.
   */
  private static String passwdToStr (String passwd) {
    if (passwd == null) return null;

    switch (passwd) {
      case "":     return "";
      case "AUTO": return "AUTO";
      case "NONE": return "NONE";
      case "GUI":  return "GUI";
      default:     return "********";
    }
  }

  /** Converts a password to a printable string that can be used in log messages. Any special
   *  values will be printed as-is while everything else will just become "********".
   *  @param passwd The password.
   *  @return The version that can be printed to the log.
   */
  private static String passwdToStr (char[] passwd) {
    if (passwd == null) return null;
    return passwdToStr(new String(passwd));
  }

  /** Converts an object (or array/collection of objects) to a string appropriate for log messages.
   *  @param cert The certificate.
   *  @return The version that can be printed to the log.
   */
  private static CharSequence toStr (Object obj) {
    return toStr(obj, "\n    ");
  }

  /** Converts an object (or array/collection of objects) to a string appropriate for log messages.
   *  @param cert The certificate.
   *  @return The version that can be printed to the log.
   */
  @SuppressWarnings("unchecked")
  private static CharSequence toStr (Object obj, String indent) {
    if (obj == null) {
      return null;
    }
    if (obj instanceof X509Certificate) {
      return ((X509Certificate)obj).getSubjectX500Principal().getName();
    }
    if (obj instanceof Certificate) {
      return "non-X509Certificate type="+((Certificate)obj).getType();
    }
    if (obj instanceof Properties) {
      // We want the properties to be sorted
      Enumeration<?>     e   = ((Properties)obj).propertyNames();
      Map<String,String> map = new TreeMap<>();

      while (e.hasMoreElements()) {
        String key = e.nextElement().toString();
        map.put(key, ((Properties)obj).getProperty(key));
      }
      obj = map;
    }
    if (obj instanceof Map) {
      StringBuilder str = new StringBuilder(1024);

      for (Map.Entry<?,?> e : ((Map<?,?>)obj).entrySet()) {
        String key = e.getKey().toString();
        Object val = toStr(e.getValue());

        str.append(indent).append(key);
        for (int i = key.length(); i < 32; i++) {
          str.append(' ');
        }
        str.append(" = ");
        str.append(toStr(val, indent+"  "));
      }
      return str;
    }
    if (obj instanceof Iterable) {
      StringBuilder str = new StringBuilder(1024);
      for (Object o : (Iterable<?>)obj) {
        str.append(indent).append(toStr(o, indent+"  "));
      }
      return str;
    }
    if (obj.getClass().isArray()) {
      int           len = Array.getLength(obj);
      StringBuilder str = new StringBuilder(1024);

      for (int i = 0; i < len; i++) {
        str.append(indent).append(toStr(Array.get(obj,i), indent+"  "));
      }
      return str;
    }
    // Nothing special applies...
    return obj.toString();
  }

  /** Gets the password for a key store.
   *  @param keyStoreFile       The key store file.
   *  @param keyStorePasswd     The user-supplied password (or "AUTO", "GUI", "NONE").
   *  @param keyStorePasswdFile The file with a user-supplied password.
   *  @return The password.
   */
  private static char[] getKeyStorePasswd (String keyStoreFile, String keyStorePasswd,
                                                                String keyStorePasswdFile) {
    if (keyStorePasswdFile != null) return passwdFromFile(keyStorePasswdFile);
    if (keyStorePasswd     == null) return null;

    switch (keyStorePasswd) {
      case "AUTO": return Shell.passwdFromGUI("Enter password for "+keyStoreFile); // Changed to use new graphical password popup in Shell.java since 3.9.1 for Java 11 modularity
      case "GUI":  return Shell.passwdFromGUI("Enter password for "+keyStoreFile); // Changed to use new graphical password popup in Shell.java since 3.9.1 for Java 11 modularity
      case "NONE": return null;
      default:     return keyStorePasswd.toCharArray();
    }
  }

  /** Gets the password for a trust store.
   *  @param trustStoreFile       The trust store file.
   *  @param trustStorePasswd     The user-supplied password (or "AUTO", "GUI", "NONE").
   *  @param trustStorePasswdFile The file with a user-supplied password.
   *  @return The password.
   */
  private static char[] getTrustStorePasswd (String trustStoreFile, String trustStorePasswd,
                                                                    String trustStorePasswdFile) {
    if (trustStorePasswdFile != null) return passwdFromFile(trustStorePasswdFile);
    if (trustStorePasswd     == null) return null;

    switch (trustStorePasswd) {
      case "AUTO": return null;
      case "GUI":  return Shell.passwdFromGUI("Enter password for "+trustStoreFile);  // Changed to use new graphical password popup in Shell.java since 3.9.1 for Java 11 modularity
      case "NONE": return null;
      default:     return trustStorePasswd.toCharArray();
    }
  }

  /** Looks through a comma-separated list of files to find the first one that matches a file on
   *  disk or on the Java class path. This will also perform the automatic substitution on
   *  ${HOME}, {$USER}, and ${HOST}.
   *  @param fileList The list of files.
   *  @return The first match or null if none match.
   *  @since NeXtMidas 3.7.3
   */
  private static String findFile (String fileList) {
    if (fileList == null) return null;

    fileList = fileList.trim();

    if (fileList.startsWith("[[") && fileList.endsWith("]]")) {
      // Inline file content.
      return fileList;
    }

    String host = "${HOST}";
    try {
      InetAddress ia = InetAddress.getLocalHost();
      host = ia.getHostName();
      int i = host.indexOf('.');
      if (i > 0) host = host.substring(0,i); // changes "myserver.example.com" to "myserver"
    }
    catch (UnknownHostException e){
      Shell.printStackTrace("Unable to get host name", e);
    }
    for (String fileName : fileList.split(",")) {
      String fname = fileName.trim().replace("${HOME}", Shell.getProperty("user.home", "${HOME}"))
                                    .replace("${USER}", Shell.getProperty("user.name", "${USER}"))
                                    .replace("${HOST}", host);
      if (fname.isEmpty()) {
        // skip
      }
      else if (new File(fname).exists()) {
        return fname;
      }
      else if (SSLUtilities.class.getClassLoader().getResource(fname) != null) {
        return fname;
      }
      else {
        // Handle NeXtMidas specific paths (e.g. nxm.sys.test)
        // Use BaseFile to handle paths with 'classpath:'
        BaseFile bf = new BaseFile(fname);
        String fileUrl = bf.getURL();
        boolean fileExists = bf.exists();
        bf.close();
        // if it exists, return file url String
        if (fileExists) {
          return fileUrl;
        }
      }
    }
    return null;
  }

  /** Gets Client Attributes for REST service providing Access Control. <br>
   *  <br>
   *  Required: Properties file must have the properties related to User JSON attribute
   *  checks defined.
   *  @param clientDN distinguished name of user/client for whom attributes
   *                  (i.e. right to access) is being checked
   *                  (cannot be null)
   *  @return JSON containing user/client attributes
   *  @since NeXtMidas 3.7.3
   *  @throws IllegalStateException If this method is called before {@code initSSLContext(..)} is
   *                                complete.
   */
  @ProvisionalUseOnly("This method is experimental and will likely change.")
  public static CharSequence getClientAttributes (String clientDN) {
    checkInit();
    requireNonNull(clientDN, "Client Distinguished Name (DN) must be specified");
    if (StringUtil.isNullOrEmpty(userQuery)) {
      throw new MidasException(" In order to verify a a client's attributes, the" +
                                "the properties file must define: SSLUtilities.userQuery");
    }

    try {
      // Replace any spaces in user's distinguished name with %20
      String cleanClientDN = clientDN.replace(" ","%20");
      URL url = (new URI(String.format(userQuery,cleanClientDN))).toURL();
      return downloadJson(url);
    }
    catch (IOException | URISyntaxException ioe) {
      throw new MidasException("Unable to get client attributes.", ioe);
    }
  }

  /** Verifies that the user/client is a member of the required group on the
   *  project specified. <br>
   *  <br>
   *  Required: Properties file must have the properties related to
   *  Group/Project and JSON attribute checks defined
   *
   *  @param clientDN  DN from client/user appropriate certificate
   *                   (cannot be null)
   *  @param group     required group within project(cannot be null)
   *  @param project   name of project (cannot be null)
   *  @return          true is client/user is in the appropriate group
   *  @since NeXtMidas 3.7.3
   *  @throws IllegalStateException If this method is called before {@code initSSLContext(..)} is
   *                                complete.
   */
  @ProvisionalUseOnly("This method is experimental and will likely change.")
  public static boolean verifyGroupProject (String clientDN, String group, String project) {
    checkInit();
    if ((clientDN == null) || (group == null) || (project == null)) {
      throw new MidasException("In order to verify that a client is a member of the required " +
                               "group, client DN, project, and group must be specified - user " +
                               "DN:" + userDN + " group:" + group + " project:"+project);
    }
    if (StringUtil.isNullOrEmpty(groupQuery)) {
      throw new MidasException("In order to verify that a client is a member of the required group, "+
                               "the properties file must define: SSLUtilities.groupQuery");
    }
    // Replace any spaces in user's distinguished name with %20
    String cleanClientDN = clientDN.replace(" ","%20");
    String cleanProject  = project.replace(" ","%20");

    try {
      URL url = (new URI(String.format(groupQuery, cleanClientDN,cleanProject))).toURL();
      CharSequence jsonCharSeq = downloadJson(url);

      String  projectJsonKeyPath = jsonKeyPaths.get("project_group");
      String  groupValue         = String.format("cn=%s", group);
      boolean isInGroup          = jsonObjHasAttribute(jsonCharSeq, groupValue, projectJsonKeyPath, true);

      return isInGroup;
    }
    catch (IOException | URISyntaxException ioe) {
      throw new MidasException("ERROR connecting and obtaining project/group info.", ioe);
    }
  }

  /** Get List of Valid User Certificates based on a specific TrustStore
   *
   *  @param trustStoreFile  full path of TrustStore file
   *  @param pwTrustStore    TrustStore password
   *  @return                ArrayList of the certificates that are valid by DN
   *                         (distinguished name)
   */
  @InternalUseOnly
  public static ArrayList<String> validateLocalCerts (String trustStoreFile, String pwTrustStore) {
    return validateLocalCerts(trustStoreFile, pwTrustStore, CertificateProperty.DN, false);
  }

  /** Get List of Valid User Certificates based on a specific TrustStore
   *
   *  @param trustStoreFile  full path of TrustStore file
   *  @param pwTrustStore    TrustStore password
   *  @param certProp        The certificate property to include in the list
   *                         specified as a CertificateProperty
   *  @param verbose         Enabling logging of status of each validation
   *  @return                ArrayList of the certificates that are valid
   *                         listed by property specified
   */
  @InternalUseOnly
  public static ArrayList<String> validateLocalCerts (String trustStoreFile, String pwTrustStore,
      CertificateProperty certProp, boolean verbose) {
    ArrayList<String> names = new ArrayList<>();
    if (!Shell.isWindows()) {
      LogUtilities.log(SSLUtilities.class, midas, java.util.logging.Level.WARNING,
          "Validating auto-discovered local certificates is only supported in Windows");
      return names;
    }
    try {
      KeyStore keyStore = KeyStore.getInstance("Windows-MY", "SunMSCAPI");
      keyStore.load(null, null);
      return validateCerts(trustStoreFile, pwTrustStore, keyStore, certProp, verbose);
    }
    catch (KeyStoreException | NoSuchProviderException | NoSuchAlgorithmException | CertificateException | IOException e) {
      if (verbose) LogUtilities.log(SSLUtilities.class, midas, java.util.logging.Level.INFO,
          "Exception loading keystore with local certificates",e);
    }
    return names;
  }

  /** Get List of Valid Certificates based on a specific TrustStore
  *
  *  @param trustStoreFile  full path of TrustStore file
  *  @param pwTrustStore    TrustStore password
  *  @param keyStore        certificates to validate
  *  @param certProp        The certificate property to include in the list
  *                         specified as a CertificateProperty
  *  @param verbose         Enabling logging of status of each validation
  *  @return                ArrayList of the certificates that are valid
  *                         listed by property specified
  */
  @InternalUseOnly
  public static ArrayList<String> validateCerts (String trustStoreFile, String pwTrustStore,
      KeyStore keyStore, CertificateProperty certProp, boolean verbose) {
    char[] trustpw = (pwTrustStore == null) ? null : pwTrustStore.toCharArray();
    List<X509TrustManager> trustMgrs = new ArrayList<>(8); // associated with trust store
    if (trustStoreFile != null) {
      try (BaseFile truststoreFile = new BaseFile(midas,trustStoreFile)) {
        InputStream inStreamTrust = truststoreFile.findResource(IOResource.DIR_INPUT).getInputStream();

        // ==== LOAD THE TRUST STORE ===========================================================
        KeyStore ts = KeyStore.getInstance(KeyStore.getDefaultType());
        ts.load(null);
        ts.load(inStreamTrust, trustpw);


        // ==== GET THE TRUST MANAGERS =========================================================
        TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
        tmf.init(ts);
        for (TrustManager tm : tmf.getTrustManagers()) {
          if (tm instanceof X509TrustManager) {
            trustMgrs.add((X509TrustManager)tm);
          }
        }
      }
      catch (GeneralSecurityException | IOException e) {
        if (verbose) LogUtilities.log(SSLUtilities.class, midas, java.util.logging.Level.INFO,
            "Exception loading truststore when validating certificates",e);
      }
    }
    return validateCerts(trustMgrs, keyStore, certProp, verbose);
  }

  /** Get List of Valid Certificates based on a specific TrustStore
   *
   *  @param trustMgrs trust managers associated with the truststore
   *  @param keyStore  keystore containing
   *  @param certProp  The certificate property to include in the list
   *                   specified as a CertificateProperty
   *  @param verbose   Enabling logging of status of each validation
   *  @return          ArrayList of the certificates that are valid
   *                   listed by property specified
   */
  @InternalUseOnly
  public static ArrayList<String> validateCerts (List<X509TrustManager> trustMgrs, KeyStore keyStore,
                                                 CertificateProperty certProp, boolean verbose) {
    ArrayList<String> properties = new ArrayList<>();
    try {
      Enumeration<String> aliases = keyStore.aliases();

      while(aliases.hasMoreElements()) {
        String logMsg = "";
        String alias = aliases.nextElement();
        if (verbose) logMsg += " alias: " + alias;
        Certificate cert = keyStore.getCertificate(alias);
        if (cert instanceof X509Certificate) {
          String name = ((X509Certificate)cert).getSubjectX500Principal().getName();
          if (verbose) logMsg += " DN: " + name;
          Certificate[] chain = keyStore.getCertificateChain(alias);
          ArrayList<X509Certificate> list509 = new ArrayList<>();
          for (int i=0; i < chain.length; i++) {
            if (chain[i] instanceof X509Certificate) {
              list509.add((X509Certificate) chain[i]);
            }
          }
          X509Certificate[] chain509 = list509.toArray(new X509Certificate[0]);
          for (X509TrustManager trustMgr : trustMgrs) {
            try {
              String property;
              switch (certProp) {
                case ALIAS: property = alias; break;
                case DN:    property = name; break;
                default:    property = alias; break;
              }
              trustMgr.checkClientTrusted(chain509, cert.getType());
              properties.add(property);
              if (verbose) logMsg = "Passed Certificate Validation" + logMsg;
            }
            catch (Exception e) {
              if (verbose) logMsg = "Failed Certificate Validation" + logMsg ;
            }
            if (verbose) {
              logMsg += " trust manager:"+trustMgr;
              LogUtilities.log(SSLUtilities.class, midas, java.util.logging.Level.INFO, logMsg);
            }
          }
        }
      }
    }
    catch (KeyStoreException e) {
      if (verbose) LogUtilities.log(SSLUtilities.class, midas, java.util.logging.Level.INFO, "Exception validating certificates",e);
    }
    return properties;
  }

  /** Get enum value via String representation
   *  Used for macro access only
   *  @param prop CertificateProperty name as String
   *  @return CertificateProperty matching that name
   */
  @InternalUseOnly
  public static CertificateProperty getCertificateProperty (String prop) {
    return nxm.sys.lib.SSLUtilities.CertificateProperty.valueOf(prop);
  }

  /** Verifies that the JSON Object String with the User information contains the proper attribute.
   *  This is identical to {@code verifyUserAttribute(jsonObjCharSeq, attributeName, attributeValue,
   *  false)}.
   *  @param <T>               attribute value Type
   *  @param jsonObjCharSeq    JSON Object in String form containing user information.
   *  @param attributeName     key or array name for attribute (category) (case-insensitive).
   *  @param attributeValue    actual attribute value required (either from key/value pair of
   *                           value in a JSON array) (case-sensitive)
   *  @return true if the attribute is listed in JSON Object String, false otherwise
   *  @since NeXtMidas 3.7.3
   *  @throws IllegalStateException If this method is called before {@code initSSLContext(..)} is
   *                                complete.
   */
  @ProvisionalUseOnly("This method is experimental and will likely change.")
  public static <T> boolean verifyUserAttribute (CharSequence jsonObjCharSeq, String attributeName,
                                                 T attributeValue) {
    return verifyUserAttribute(jsonObjCharSeq, attributeName, attributeValue, false);
  }

  /** Verifies that the JSON Object String with the User information contains the proper attribute.
   *  @param <T>               attribute value Type
   *  @param jsonObjCharSeq    JSON Object in String form containing user information.
   *  @param attributeName     key or array name for attribute (category) (case-insensitive).
   *  @param attributeValue    actual attribute value required (either from key/value pair of
   *                           value in a JSON array) (case-sensitive)
   *  @param allowPartialMatch Set this to true to allow a partial match on String values, where
   *                           the required value is a substring contained within the JSON value,
   *                           set this to false to require an exact match
   *  @return true if the attribute is listed in JSON Object String, false otherwise
   *  @since NeXtMidas 3.7.3
   */
  @ProvisionalUseOnly("This method is experimental and will likely change.")
  public static <T> boolean verifyUserAttribute (CharSequence jsonObjCharSeq, String attributeName,
                                                 T attributeValue, boolean allowPartialMatch) {
    String pathToAttr = jsonKeyPaths.get(attributeName.toLowerCase());
    return jsonObjHasAttribute(jsonObjCharSeq, attributeValue, pathToAttr, allowPartialMatch);
  }

  /** Allow a specified properties file to load jsonKeyPaths.
   *  This will override the ones loaded from the SSLUtilities file
   *  if that has already bee loaded
   *
   *  @param jsonPathProps name of properties file to use
   */
  @InternalUseOnly
  public static void overideJsonKeyPaths (String jsonPathProps) {
    jsonKeyPaths.clear();
    if ((jsonPathProps == null) || (jsonPathProps.isEmpty())) return;
    Properties props = getProps(null, jsonPathProps);
    Enumeration<?> propNames = props.propertyNames();
    while (propNames.hasMoreElements()) {
      String key = propNames.nextElement().toString();
      String val = props.getProperty(key);

      if (key.startsWith("SSLUtilities.responsePath.")) {
        String jsonKey = key.replaceFirst("SSLUtilities.responsePath.","");
        jsonKeyPaths.put(jsonKey.toLowerCase(), val);
      }
    }
  }

  /** Verify a JSON response contains the sepcified attributes
   *
   *  @param jsonResponse     JSON response to validate against
   *  @param attrValueTbl       Attributes it needs to contain
   *  @param allowPartialMatch  Is a partial match allowed
   *  @return true if attributes specified in Table are contained in JSON
   */
  @ProvisionalUseOnly("in evaluation")
  public static boolean verifyJsonHasAttributes (CharSequence jsonResponse,
      Table attrValueTbl, boolean allowPartialMatch) {
    boolean authorized = false;
    boolean noAttributeFailures = true;
    if (attrValueTbl != null) {
      boolean activeKeyPaths = !jsonKeyPaths.isEmpty();
      for (String attr: attrValueTbl) {
        // get path to attribute in JSON, but default to the path being the attribute name
        String jsonKeyPath = (activeKeyPaths) ? jsonKeyPaths.get(attr) : null;
        final String pathToAttr = (jsonKeyPath==null) ? attr : jsonKeyPath;
        // check if JSON has the specified attribute
        boolean thisAttributeIsValid = SSLUtilities
            .jsonObjHasAttribute(jsonResponse, attrValueTbl.get(attr),
                pathToAttr, allowPartialMatch);
        LogUtilities.log(SSLUtilities.class, midas, certInfoLogLevel,
            () ->"SSLUtilities.verifyJsonHasAttributes: attr:"+attr+" value:"+
                pathToAttr+" valid:"+thisAttributeIsValid);
        if (!thisAttributeIsValid) {
          noAttributeFailures = false;
          break;
        }
      }
      authorized = noAttributeFailures;
    }
    else {
      // validation is based just on the user attributes existing
      authorized =  ((jsonResponse != null) &&
          (jsonResponse.length() == 0));
    }
    return authorized;
  }

  /** Reads JSON content from the provided URL.
   *  @param url The URL to download.
   *  @return The JSON content in string form. Note that no explicit validation is performed.
   *  @throws IOException If there is an error while downloading.
   *  @since NeXtMidas 3.7.3
   */
  @InternalUseOnly
  public static CharSequence downloadJson (URL url) throws IOException {
    URLConnection conn     = url.openConnection();
    URL.setURLStreamHandlerFactory(null);

    LogUtilities.log(SSLUtilities.class, midas, certInfoLogLevel, "Downloading JSON from {0}", url);

    // Some servers have options for other output formats (e.g. XML) and/or use some form
    // of protection against XSRF attacks. This variability is undesired, so we specifically
    // indicate that we want JSON with no XSRF protection (normal usage of this library is
    // immune to XSRF attacks, so the the XSRF protection only serves to make a mess of the
    // JSON content in the response).
    //
    // We currently disable caching to avoid any cache-related issues, though this will be
    // revisited in the future. Note that caching is a tricky topic since cache expiration
    // will need to be user controlled to comply with rules concerning the lifespan of an
    // authorization (e.g. if a user's authorization is revoked, we need to pick it up in
    // a timely manner; we will also want to pick up new authorizations added in a timely
    // manner to support new users).
    //
    // Note that we explicitly use setRequestProperty(..) and avoid addRequestProperty(..).
    conn.setRequestProperty("Accept",               "aplication/json, text/json, */*");
    conn.setRequestProperty("X-XSRF-UseProtection", "false");
    conn.setRequestProperty("Accept-Language",      "en-US,en;q=0.5");
    conn.setUseCaches(false);

    // the next two lines must come before connect()
    Map<String, List<String>> requestProperties = conn.getRequestProperties();
    boolean                   usesCaches        = conn.getUseCaches();

    conn.connect();

    if (conn instanceof HttpURLConnection) {
      int responseCode = ((HttpURLConnection)conn).getResponseCode();

      LogUtilities.log(SSLUtilities.class, midas, certInfoLogLevel,
          () -> logConnection(url,conn,requestProperties, usesCaches, responseCode));


      if (responseCode != HttpURLConnection.HTTP_OK) {
        throw new IOException("Connection to "+url+" returned "+responseCode+" (expected "+
                              HttpURLConnection.HTTP_OK+")");
      }
    }

    InputStream in  = conn.getInputStream();
    byte[]      buf = new byte[32768];
    int         off = 0;
    int         num;

    while ((num = in.read(buf, off, buf.length-off)) >= 0) {
      buf = Arrays.copyOf(buf, buf.length+32768);
      off += num;
    }

    String json = new String(buf, 0, off);

    LogUtilities.log(SSLUtilities.class, midas, certInfoLogLevel, () ->
       "\n+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n" + json +
       "\n+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n");


    return json;
  }

  private static String logConnection (URL url, URLConnection conn,  Map<String, List<String>> requestProperties,
      boolean usesCaches, int responseCode) {
    StringBuilder debugStr = new StringBuilder(32768);
    debugStr.append("\n++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n");
    debugStr.append("SSLUtilities.downloadJson(..):\n");
    debugStr.append("  url                          = ").append(url).append('\n');
    debugStr.append("  conn                         = ").append(conn).append('\n');
    debugStr.append("  conn.getRequestProperties()  = ").append(requestProperties).append('\n');
    debugStr.append("  conn.getUseCaches()          = ").append(usesCaches        ).append('\n');
    debugStr.append("  conn.getHeaderFields()       = ").append(conn.getHeaderFields()     ).append('\n');
    debugStr.append("  conn.getResponseCode()       = ").append(responseCode               ).append('\n');

    if (conn instanceof HttpsURLConnection) {
      HttpsURLConnection c = (HttpsURLConnection)conn;

      try {
        debugStr.append("  conn.getCipherSuite()        = ").append(c.getCipherSuite()     ).append('\n');
        debugStr.append("  conn.getHostnameVerifier()   = ").append(c.getHostnameVerifier()).append('\n');
        debugStr.append("  conn.getLocalPrincipal()     = ").append(c.getLocalPrincipal()  ).append('\n');
        debugStr.append("  conn.getPeerPrincipal()      = ").append(c.getPeerPrincipal()   ).append('\n');
        debugStr.append("  conn.getSSLSocketFactory()   = ").append(c.getSSLSocketFactory()).append('\n');
        debugStr.append("  conn.getLocalCertificates()  = ").append(SSLUtilities.toStr(c.getLocalCertificates() )).append('\n');
        debugStr.append("  conn.getServerCertificates() = ").append(SSLUtilities.toStr(c.getServerCertificates())).append('\n');
      }
      catch (SSLPeerUnverifiedException ssle) {
        throw new MidasException("ERROR getting HttpsURLConnection information.", ssle);
      }
    }
    debugStr.append("+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n");
    return debugStr.toString();
  }

  /** Verifies that the JSON String has the attribute value specified.
   *  @param <T>                attribute value Type
   *  @param jsonObjStr         JSON in String form
   *  @param attribute          required value of attribute
   *  @param pathToAttr         path to attribute (comma separated list of keys)
   *  @param allowPartialMatch  true:  required value of attribute is a String
   *                                   and just needs to be contained in the
   *                                   JSON value
   *                            false: exact match is required
   *  @return true if attribute value is found in JSON (i.e. user has required
   *          attribute)
   *  @since NeXtMidas 3.7.3
   */
  @ProvisionalUseOnly("This method is experimental and will likely change.")
  @SuppressWarnings({"rawtypes","unchecked"})
  public static <T> boolean jsonObjHasAttribute (CharSequence jsonObjStr, T attribute,
                                                 String pathToAttr, boolean allowPartialMatch) {
    LogUtilities.log(SSLUtilities.class, midas, certInfoLogLevel,
         "attribute: {0} pathToAttr:{1} partial match allowed:{2}\n json:{3}",
         attribute, pathToAttr, allowPartialMatch, jsonObjStr);
    if (attribute == null || pathToAttr == null) return false;
    boolean  hasAttr  = false;
    String[] jsonKeys = pathToAttr.split("[,\\s]+");
    LinkedList<String> listJsonKeys = new LinkedList<>(Arrays.asList(jsonKeys));

    try {
      LinkedHashMap<String,Object> jsonAsMap = JsonUtilities.fromJSON(LinkedHashMap.class,jsonObjStr);

      String currKey  = listJsonKeys.removeFirst();
      Object innerObj = jsonAsMap.get(currKey);

      while (!listJsonKeys.isEmpty() && (innerObj instanceof Map)) {
        currKey = listJsonKeys.removeFirst();
        innerObj = ((Map)innerObj).get(currKey);
      }
      if (listJsonKeys.isEmpty()) {
        if (innerObj instanceof ArrayList) {
          ArrayList<T> attrArray = (ArrayList<T>) innerObj;
          hasAttr = attrArray.contains(attribute);

          // check for partial matches on Strings only
          if (!hasAttr & allowPartialMatch && (attribute instanceof CharSequence)) {
            int arrayIndex = 0;
            while (!hasAttr && arrayIndex < attrArray.size()) {
              T entry = attrArray.get(arrayIndex);
              if (entry != null && entry instanceof String) {
                hasAttr = ((String)entry).contains((CharSequence)attribute);
              }
              arrayIndex++;
            }
          }
        }
        else {
          if ((innerObj != null) && (innerObj.getClass() == Boolean.class) &&
              (attribute.getClass() == String.class) &&
              (((String)attribute).equalsIgnoreCase("true") ||
              ((String)attribute).equalsIgnoreCase("false"))) {
            // handle case where JSON value is Boolean and expected is "true" or "false"
            Boolean boolAttr = Boolean.valueOf((String)attribute);
            hasAttr = boolAttr.equals(innerObj);

          }
          else {
            hasAttr = attribute.equals(innerObj);
          }
          // Check for partial matches on Strings only
          if (!hasAttr & allowPartialMatch && (attribute instanceof CharSequence)
              && (innerObj instanceof String)) {
            hasAttr = ((String)innerObj).contains((CharSequence)attribute);
          }
        }
      }
    }
    catch (IllegalArgumentException iae) {
      throw new MidasException("SSLUtilities parameter jsonObjStr must be JSON Object:", iae);
    }
    return hasAttr;
  }

  /** Retrieves the private key password from a file
   *  Note: password is assumed to in first line of file
   *  @param keyStorePasswdFile path/name of file containing password
   *  @return The password from the file.
   *  @since NeXtMidas 3.7.3
   */
  @InternalUseOnly("This is public only to support testing.")
  public static char[] passwdFromFile (String keyStorePasswdFile) {
    String userPwd;
    FileName fnKeyStorePasswdFile = new FileName(keyStorePasswdFile,FNCase.KeepCase);
    try (TextFile tf = new TextFile(fnKeyStorePasswdFile)) {
      tf.open(TextFile.INPUT|TextFile.FORCEABORT);
      userPwd = tf.read();
      userPwd = userPwd.replaceAll("\\s", ""); // remove any white space
    }
    return userPwd.toCharArray();
  }

  /** Check to see if initialization has been completed by the {@code initSSLContext(..)} method.
   *  @return true if {@code initSSLContext(..)} has completed successfully and false otherwise.
   *  @since NeXtMidas 3.7.3
   */
  public static boolean isInitDone () {
    return (initState == STATE_DONE);
  }

  /** Verifies that initialization is done.
   *  @throws IllegalStateException If this method is called before {@code initSSLContext(..)} is
   *                                complete.
   *  @since NeXtMidas 3.7.3
   */
  private static void checkInit () {
    switch (initState) {
      case STATE_INIT:   throw new IllegalStateException("Not initialized.");
      case STATE_DONE:   break;
      case STATE_FAILED: throw new IllegalStateException("Not initialized.");
      default:           throw new AssertionError("Illegal state: "+initState);
    }
  }

  /** Gets Distinguished Name extracted from certificate loaded during initialization in the
   *  {@code initSSLContext(..)} method.
   *  @return DN extracted from certificate or null if not loaded.
   *  @throws IllegalStateException If this method is called before {@code initSSLContext(..)} is
   *                                complete.
   *  @since NeXtMidas 3.7.3
   */
  public static String getUserDN () {
    checkInit();
    return (allCertInfo.isEmpty())? null : allCertInfo.get(0).getUserDN();
  }

  /** Gets Common Name extracted from certificate loaded during initialization in the
   *  {@code initSSLContext(..)} method. If no common name is found, the distinguished name
   *  will be returned.
   *  @return CN extracted from certificate or null if not loaded.
   *  @throws IllegalStateException If this method is called before {@code initSSLContext(..)} is
   *                                complete.
   *  @since NeXtMidas 3.7.3
   */
  public static String getUserCN () {
    checkInit();
    return (allCertInfo.isEmpty())? null : allCertInfo.get(0).getUserCN();
  }

  /** Gets Common Name Alias extracted from certificate loaded during initialization in the
   *  {@code initSSLContext(..)} method.
   *  @param normalize Normalize the alias to match JAR signing rules?
   *  @return User alias extracted from certificate or null if not loaded.
   *  @throws IllegalStateException If this method is called before {@code initSSLContext(..)} is
   *                                complete.
   *  @since NeXtMidas 3.7.3
   */
  public static String getUserAlias (boolean normalize) {
    checkInit();
    return (allCertInfo.isEmpty())? null : allCertInfo.get(0).getUserAlias(normalize);
  }

  /** Loads a KeyStore using the information from a PEM (.pem) file
   *  If the .pem file contains multiple private key/certificate(s) pairs, then
   *  the private key location must be consistent and must always be
   *  at the beginning or must always be at the end of the key/certificate
   *  information. Within a specific key/certificate, the last (and presumably
   *  root certificate) is the one chosen.
   *  @param fileName  The file name (non-null)
   *  @param algorithm The algorithm (if null, "RSA" is used)
   *  @return The key store
   */
  private static KeyStore readPemFilePreferLast (String fileName, String algorithm) {
    if (algorithm == null) algorithm = "RSA";

    try (TextFile tf = new TextFile(null, fileName)) {
      tf.open();
      KeyFactory          kf          = KeyFactory.getInstance(algorithm);
      CertificateFactory  cf          = CertificateFactory.getInstance("X.509");
      X509Certificate     certificate = null;
      PublicKey           publicKey   = null;
      PrivateKey          privateKey  = null;
      LinkedList<String>  lines       = new LinkedList<>(Arrays.asList(tf.readAllLines()));
      KeyStore           ks           = KeyStore.getInstance("PKCS12");
      // load the keystore here so it is not loaded more than once.
      ks.load(null, null);
      java.util.Base64.Decoder decoder = java.util.Base64.getDecoder(); // Since 3.9.0
      int                certKeyPair  = 0; // can have multiple certificate(s)/key groupings in file
      boolean            certReady    = false;
      boolean            privateKeyOccursFirst = false;

      while (!lines.isEmpty()) {
        // retrieve line and get rid of end of line characters (needed for Windows)
        String line = lines.pop().replaceAll("(\\r|\\n)", "");
        if (line.startsWith("-----BEGIN ")) {
          if (line.endsWith("CERTIFICATE-----")) {
            byte[]    bytes = decoder.decode(readContent(lines)); // Since 3.9.0
            certificate = (X509Certificate)cf.generateCertificate(new ByteArrayInputStream(bytes));
            certReady = true;
          }
          else if (line.endsWith("PUBLIC KEY-----")) {
            byte[]    bytes = decoder.decode(readContent(lines)); // Since 3.9.0
            EncodedKeySpec spec = new X509EncodedKeySpec(bytes);
            publicKey = kf.generatePublic(spec);
          }
          else if (line.endsWith("PRIVATE KEY-----")) {
            byte[]    bytes = decoder.decode(readContent(lines)); // Since 3.9.0
            EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);
            PrivateKey previousPrivateKey = privateKey;
            privateKey = kf.generatePrivate(spec);
            if (certReady) {
              certKeyPair++; // increment count of certificate(s)/key groupings
              // add associated keys and certificates as entry in KeyStore
              if (privateKeyOccursFirst) {
                // private key at top
                addPEMCertToKeystore(ks, certificate,
                    publicKey, previousPrivateKey, tf, certKeyPair);
              }
              else {
                // private key is at end
                addPEMCertToKeystore(ks, certificate,
                    publicKey, privateKey, tf, certKeyPair);
                privateKey  = null;
              }
              // clear in case this PEM contains multiple key/certificate(S) pairs
              certReady   = false;
              publicKey   =  null;
              certificate = null;
            }
            else {
              // private key occurs before certificate(s)
              privateKeyOccursFirst = true;
            }
          }
        }
      }
      if (certReady) {
        // add last key/certificate(s) entry for case where Private Key occurs
        // before certificate(s)
        certKeyPair++; // increment count of certificate(s)/key groupings
        addPEMCertToKeystore(ks, certificate,
            publicKey, privateKey, tf, certKeyPair);
      }
      return ks;
    }
    catch (Exception e) {
      throw new MidasException("Error reading "+fileName, e);
    }
  }


  /** Loads PemFile using the information from a PEM (.pem) file
   *  If the .pem file contains multiple private key/certificate(s) pairs, then
   *  the private key location must be consistent and must always be
   *  at the beginning or must always be at the end of the key/certificate
   *  information. This update builds the full chain (NeXtMidas 4.1.4)
   *  @param fileName  The file name (non-null)
   *  @param algorithm The algorithm (if null, "RSA" is used)
   *  @return PemFile object containing key store and more
   */
  public static PemFile readPemFile (String fileName, String algorithm) {
    if (algorithm == null) algorithm = "RSA";

    try (TextFile tf = new TextFile(null, fileName)) {
      tf.open();
      KeyFactory          kf           = KeyFactory.getInstance(algorithm);
      CertificateFactory  cf           = CertificateFactory.getInstance("X.509");
      ArrayList<X509Certificate>                 certificates = new ArrayList<>(8);
      PublicKey           publicKey    = null;
      PrivateKey          privateKey   = null;
      LinkedList<String>  lines        = new LinkedList<>(Arrays.asList(tf.readAllLines()));
      Decoder             decoder      = java.util.Base64.getDecoder(); // Since 3.9.0
      String              alias        = "pem-file-"+tf.getName().getBasename();
      byte[]              bytes;

      while (!lines.isEmpty()) {
        // retrieve line and get rid of end of line characters (needed for Windows)
        String line = lines.pop().replaceAll("(\\r|\\n)", "");
        if (line.startsWith("-----BEGIN ")) {
          if (line.endsWith("CERTIFICATE-----")) {
            bytes = decoder.decode(readContent(lines)); // Since 3.9.0
            certificates.add((X509Certificate)cf.generateCertificate(new ByteArrayInputStream(bytes)));
          }
          else if (line.endsWith("PUBLIC KEY-----")) {
            if (publicKey != null) {
              LogUtilities.log(Level.WARNING, "Multiple public keys found in "+fileName+", using the first one");
            }
            else {
              bytes = decoder.decode(readContent(lines)); // Since 3.9.0
              publicKey = kf.generatePublic(new X509EncodedKeySpec(bytes));
            }
          }
          else if (line.endsWith("PRIVATE KEY-----")) {
            if (publicKey != null) {
              LogUtilities.log(Level.WARNING, "Multiple private keys found in "+fileName+", using the first one");
            }
            else {
              bytes = decoder.decode(readContent(lines)); // Since 3.9.0
              privateKey = kf.generatePrivate(new PKCS8EncodedKeySpec(bytes));
            }
          }
        }
      }

      // When constructing the KeyStore, the following must be true, even
      // though they seem like non-sequiturs when applied to a PEM file:
      //   - certChain[0] must be the cert for the private key
      //   - A PasswordProtection instance must be specified and the password
      //     must be non-null for the PrivateKeyEntry
      //   - ks.load(null, null) must be called before setting entries
      //   - The value of the alias isn't particularly important, we just need
      //     something reasonably unique and identifiable
      X509Certificate[] certChain = buildCertChain(fileName,certificates);
      return new PemFile(fileName, alias, certChain, publicKey, privateKey);
    }
    catch (Exception e) {
      throw new RuntimeException("Error reading "+fileName, e);
    }
  }

    /** Builds up the cert chain. This looks at subject/issuer to identify where they
   *  are within the chain.
   *  @param certs All of the certs.
   *  @return The cert chain.
   */
  private static X509Certificate[] buildCertChain (String fileName, Collection<X509Certificate> certs) {
    ArrayList<LinkedList<X509Certificate>> chains = new ArrayList<>(certs.size());

    for (X509Certificate c : certs) {
      LinkedList<X509Certificate> ll = new LinkedList<>();
      chains.add(ll);
      ll.add(c);
    }

    NextI:
    for (int i = 0; i < chains.size(); i++) {
      LinkedList<X509Certificate> chainI = chains.get(i);

      if (chainI != null) {
        X500Principal subI = chainI.getFirst().getSubjectX500Principal();
        X500Principal issI = chainI.getLast().getIssuerX500Principal();

        for (int j = i+1; j < chains.size(); j++) {
          LinkedList<X509Certificate> chainJ = chains.get(j);

          if (chainJ != null) {
            X500Principal subJ = chainJ.getFirst().getSubjectX500Principal();
            X500Principal issJ = chainJ.getLast().getIssuerX500Principal();

            if (issI.equals(subJ)) {
              // J issued I
              chainJ.addAll(0, chainI);
              chains.set(i, null);
              continue NextI;
            }
            if (issJ.equals(subI)) {
              // I issued J
              chainJ.addAll(chainI);
              chains.set(i, null);
              continue NextI;
            }
          }
        }
      }
    }

    LinkedList<X509Certificate> first = null;

    for (LinkedList<X509Certificate> c : chains) {
      if (c != null) {
        if (first == null) {
          first = c;
        }
        else {
          LogUtilities.log(Level.WARNING, "Multiple cert chains found in "+fileName+" trying "+first.getFirst().getSubjectX500Principal());
        }
      }
    }
    return (first == null)? null : first.toArray(new X509Certificate[first.size()]);
  }


  /**
   * Add this certificate from .pem file to KeyStore
   * @param ks           KeyStore containing PEM Certificates
   * @param certificate  certificate to add to KeyStore
   * @param publicKey    certificate public key
   * @param privateKey   certificate private key
   * @param textFile     TextFile that is a .pem file
   * @param certNum      number of this certificate (i.e. 2 for 2nd certificate)
   * @throws NoSuchAlgorithmException If the requested algorithm is not accessible.
   * @throws CertificateException If there is a certificate error.
   * @throws IOException If there is an I/O error.
   * @throws KeyStoreException If there is a keystore error.
   * @since 3.7.3 split off from readPemFile to support multiple certs
   */
  private static void addPEMCertToKeystore(KeyStore ks, X509Certificate certificate,
      PublicKey publicKey, PrivateKey privateKey, TextFile textFile, int certNum)
          throws NoSuchAlgorithmException, CertificateException, IOException, KeyStoreException {

    if ((certificate !=null) && (publicKey == null)) {
      // In my experience (J. Czechowski), you will typically see a CERTIFICATE
      // and a PRIVATE KEY entry with no PUBLIC KEY entry.  The need for the
      // public key (below) is questionable..., but we can get it from the
      // certificate, so just do that.
      publicKey = certificate.getPublicKey();
    }

    String fileName = textFile.getName().toString();
    requireNonNull(certificate, "Missing required certificate in "+fileName);
    requireNonNull(publicKey,   "Missing required public key in "+fileName);
    requireNonNull(privateKey,  "Missing required private key in "+fileName);

    // When constructing the KeyStore, the following must be true, even
    // though they seem like non-sequiturs when applied to a PEM file:
    //   - certChain[0] must be the cert for the private key
    //   - A PasswordProtection instance must be specified and the password
    //     must be non-null for the PrivateKeyEntry
    //   - ks.load(null, null) must be called before setting entries
    //   - The value of the alias isn't particularly important, we just need
    //     something reasonably unique and identifiable
    char[]             password      = new char[0];
    String             alias         = "pem-file-"+textFile.getName().getBasename();
    if (certNum > 1) {
      alias = alias + "_" + certNum; // give distinct alias for each cert in file
    }
    Certificate[]      certChain     = { certificate };
    PasswordProtection protParam     = new PasswordProtection(password);
    PrivateKeyEntry    pemPrivateKey = new PrivateKeyEntry(privateKey, certChain);
    // Remove this from here because it loads the keystore more than once which wipes it out
    // ks.load(null, null);
    ks.setEntry(alias, pemPrivateKey, protParam);

  }

  private static String readContent (LinkedList<String> lines) {
    StringBuilder str = new StringBuilder(4096);

    while (!lines.isEmpty()) {
      String line = lines.pop();
      if (line.startsWith("-----END ")) {
        return str.toString().replaceAll("(\\r|\\n)","");
      }
      str.append(line);
    }
    return str.toString();
  }

  /** Initialize the SSL Context based on content obtained via Microsoft's Cryptographic Application
   *  Programming Interface (CAPI).
   *  @param certInfo    (I/O) The certificates found.
   *  @param keyMgrs     (I/O) Key managers to use.
   *  @param trustMgrs   (I/O) Trust managers to use.
   *  @param capiEnabled (IN)  Is CAPI support enabled?
   *  @return The number of certificates found. If multiple certificates are found the
   *          first one will be used. Returns -1 on failure or -2 if disabled.
   *  @throws RuntimeException If an error occurs. This can be disabled by setting
   *                           {@code noErrors} to false, in which case -1 will be
   *                           returned.
   *  @since NeXtMidas 3.7.3
   */
  @InternalUseOnly
  public static int tryInitCAPI (List<CertInfo> certInfo, List<KeyManager> keyMgrs,
                                  List<TrustManager> trustMgrs, boolean capiEnabled) {
    // Skip everything if we CAPI based initialization is not enabled, we are not on
    // a Windows OS, or we already have valid certificates
    if (!capiEnabled || !Shell.isWindows() || !certInfo.isEmpty()) return -2;
    try {
      KeyManagerFactory   kmf = KeyManagerFactory.getInstance("SunX509");
      TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
      KeyStore            ks  = KeyStore.getInstance("Windows-MY");
      KeyStore            ts  = KeyStore.getInstance("Windows-ROOT");
      int                 old = certInfo.size();
      ks.load(null, null);
      ts.load(null, null);
      kmf.init(ks, null);
      tmf.init(ts);

      findCertificates(certInfo, kmf, ks, null, true);
      trustMgrs.addAll(Arrays.asList(tmf.getTrustManagers()));

      return certInfo.size() - old;
    }
    catch (Exception e) {
      LogUtilities.log(SSLUtilities.class, midas, Level.SEVERE,
          "tryInitCAPI: Error initializing SSL Context based on content obtained via Microsoft's CAPI", e);
      return -1;
    }
  }

  /** Initialize the SSL Context based on content obtained via Firefox's Network
   *  Security Services
   *  @param certInfo       (I/O)  The certificates found.
   *  @param keyMgrs        (I/O)  Key managers to use.
   *  @param trustMgrs      (I/O)  Trust managers to use.
   *  @param nssEnabled     (IN)   Is NSS support enabled?
   *  @param includeLibDir  (IN)   Include the 'nssLibraryDirectory' setting when
   *                               certificate initializing. This is more likely
   *                               to allow loading without a password, but it is
   *                               fairly unreliable in practice since it must (1)
   *                               guess the proper directory, and (2) get it working
   *                               (Java tends to have issues loading the libraries
   *                               of many Firefox installations).
   *  @return The number of certificates found. If multiple certificates are found
   *          the first one will be used.  Returns -1 on failure or -2 if disabled.
   *  @throws RuntimeException If an error occurs.  This can be disabled by setting
   *                           {@code noErrors} to false, in which case -1 will be
   *                           returned.
   * @since NeXtMidas 3.7.3 for NXM-3030
   */
  @InternalUseOnly
  public static int tryInitNSS (List<CertInfo> certInfo, List<KeyManager> keyMgrs,
                                 List<TrustManager> trustMgrs, boolean nssEnabled,
                                 boolean includeLibDir) {
    // Skip everything if we NSS based initialization is not enabled, we already
    // have valid certificates, the PCKS11 Provider has already been initialized
    if (!nssEnabled || !certInfo.isEmpty() || pkcs11Initialized) return -2;
    // includeLibsDir should always be false if nssLibsDir is not defined - since 3.9.2
    if (StringUtil.isEmpty(nssLibsDir)) includeLibDir = false;

    try {
      // This line won't work on Windows.  At this time there isn't much reason
      // to fix this since we document this as Linux only, given that most Windows
      // users will use Microsoft's CAPI (which integrates with Internet Explorer).
      String profiles = findFile("${HOME}/.mozilla/firefox/profiles.ini");
      if (profiles == null) {
        // Can't find user's Firefox configuration.
        return -1;
      }

      Table profilesTbl = Table.fromConfigFile(null, profiles);
      String profilePath = null;
      // try to get selected profile
      profilePath = getSelectedProfilePath(profilesTbl);
      // as a backup, use the profile with the name=default
      // this extra check was found to be important on a CentOS 6
      if (profilePath==null) {
        profilePath = getProfilePathNamedDefault(profilesTbl);
      }
      if (profilePath == null) {
        // Can't find user's default profile
        return -1;
      }

      // The SunPKCS11 supports NSS if given the proper settings...
      File rootDir     = new File(profiles).getParentFile();
      File profileDir  = new File(rootDir, profilePath);

      String pkcs11Provider = "NSScrypto";
      String pkcs11Config   = "name=NSScrypto\n"
                              + "nssDbMode=readOnly\n"
                              + "nssSecmodDirectory=\""+profileDir+"\"\n" // Added quotes to directory path in case of whitespace since 3.9.2
                              + "attributes=compatibility";

      if (includeLibDir) {
        // *****************************************************************
        // Note: I have not gotten this to work right, I keep getting
        //       "/usr/lib64/libnsutil3.so: version 'NSSUTIL_3.21' not found"
        //       errors.  A number of sites suggest that this is due to how
        //       my Firefox is installed.  I don't have control over that, so
        //       I've never gotten this to fully work... (J. Czechowski)
        // *****************************************************************

        // Find where Firefox is installed...
        StringBuilder out    = new StringBuilder(80);
        StringBuilder err    = new StringBuilder(80);
        int           exitCode = Foreign.runInternal("which firefox", out, err);

        if (exitCode != 0) {
          return -1;
        }

        // Resolve any symlinks...
        File firefox = new File(out.toString().trim());
        if (!firefox.exists()) {
          return -1;
        }
        try {
          firefox = firefox.getCanonicalFile();
        }
        catch (IOException e) {
          firefox = firefox.getAbsoluteFile();
        }

        // check the nssLibsDir first for the .so files
        File dir = new File(nssLibsDir);
        // Look for 'libnss*.so'
        File[] nssFiles = dir.listFiles((d,n) -> n.matches("libnss.+[.]so.*")); // Since 3.9.0
        if (nssFiles.length == 0) {
          // Try the firefox directory now
          dir = firefox.getParentFile();
          nssFiles = dir.listFiles((d,n) -> n.matches("libnss.+[.]so.*"));
          if (nssFiles.length == 0) {
            // if the nss libraries are not available, we can't use nss directory
            return -1;
          }
        }

        pkcs11Config += "\nnssLibraryDirectory=\""+dir+"\"\n";  // Added quotes to directory path in case of whitespace since 3.9.2
      }

      return tryInitPKCS11(certInfo, keyMgrs, trustMgrs, pkcs11Provider, pkcs11Config);
    }
    catch (Exception e) {
      LogUtilities.log(SSLUtilities.class, midas, Level.SEVERE,
          "tryInitCAPI: Error initializing SSL Context based on content obtained via Firefox's NSS", e);
      return -1;
    }

  }

  /** Gets the selected profile path that has a key of DEFAULT set
   *  to a value of 1
   *
   *  @param profilesTbl nss profiles table
   *  @return profile path
   */
  private static String getSelectedProfilePath (Table profilesTbl) {
    for (String key: profilesTbl.getKeys()) {
      Table t = profilesTbl.getTableOrNull(key);

      if ((t == null) || !t.containsKey("DEFAULT")) {
        // skip
      }
      else if (t.getState("DEFAULT")) {
        String profilePath = t.getS("PATH");
        if (profilePath!=null) return profilePath;
      }
    }
    return null;
  }

  /** Gets the original default profile path (that has name=default)
   *  This should be tried if none of the profiles are indicated as
   *  selected via a key of DEFAULT set to a value of 1
   *
   *  @param profilesTbl nss profiles table
   *  @return profile path
   */
  private static String getProfilePathNamedDefault (Table profilesTbl) {
    for (String key: profilesTbl.getKeys()) {
      Table t = profilesTbl.getTableOrNull(key);

      if ((t == null) || !t.containsKey("NAME") || !t.getS("NAME").equalsIgnoreCase("default")) {
        // skip
      }
      else {
        String profilePath = t.getS("PATH");
        if (profilePath!=null) return profilePath;
      }
    }
    return null;
  }

  /** Initialize the SSL Context based on PKCS11 content provider (e.g. CSPid). This can be
   *  disabled by setting pksc11Provider to null (or the empty string).
   *  @param certInfo       (I/O) The certificates found.
   *  @param keyMgrs        (I/O) Key managers to use.
   *  @param trustMgrs      (I/O) Trust managers to use.
   *  @param pkcs11Provider (IN)  The PKCS11 provider to use.
   *  @param pkcs11Config   (IN)  The PKCS11 provider configuration (ignored if the provider is
   *                              already initialized).
   *  @return The number of certificates found. If multiple certificates are found the
   *          first one will be used. Returns -1 on failure or -2 if disabled.
   *  @since NeXtMidas 3.7.3
   */
  @SuppressWarnings("AssignmentToForLoopParameter")
  @InternalUseOnly
  public static int tryInitPKCS11 (List<CertInfo> certInfo, List<KeyManager> keyMgrs,
                                    List<TrustManager> trustMgrs, String pkcs11Provider,
                                    String pkcs11Config) {
    // Skip everything if either the pksc11Provider or the pkcs11Config are not provided,
    // if we have valid certificates, or the PCKS11 Provider has already been initialized,
    if (StringUtil.isEmpty(pkcs11Provider) || StringUtil.isEmpty(pkcs11Config) ||
        !certInfo.isEmpty() || pkcs11Initialized) {
      return -2;
    }

    try {
      Provider provider = Security.getProvider(pkcs11Provider);
      if (provider == null) { // JDK 11 block since 3.9.2
        // Since JDK 11, the sun.security.pkcs11.SunPKCS11 constructor does not take parameters.
        // Need to use the configure method to instantiate the provider. This is not available in
        // jdk 8 so doing it via reflection is the trick.
        if (PROVIDER_CONFIGURE != null) {
          provider = Security.getProvider("SunPKCS11");
          // Add a -- in front of string or it will think it is a file name instead of a string config
          provider = (Provider) PROVIDER_CONFIGURE.invoke(provider, "--"+pkcs11Config);
        }
        else {
          // PKCS11 is rarely configured in advance, so if it is not found, go ahead and load it.
          Class<?> clazz = Class.forName("sun.security.pkcs11.SunPKCS11");
          // This is not an exact science... we just want to differentiate a set
          // of properties passed in from a file name
          boolean isFileName = !pkcs11Config.contains("=") &&
                               !pkcs11Config.contains("\n");
          if (pkcs11Config.startsWith("[[") && pkcs11Config.endsWith("]]")) {
            isFileName = false;
            pkcs11Config = pkcs11Config.substring(2, pkcs11Config.length()-2)
                                       .trim()
                                       .replaceAll(";[ ]*", "\n");
          }

          if (!isFileName) {
            Constructor<?> con = clazz.getConstructor(InputStream.class);
            ByteArrayInputStream in = new ByteArrayInputStream(pkcs11Config.getBytes());
            provider = (Provider)con.newInstance(in);
          }
          else {
            Constructor<?> con = clazz.getConstructor(String.class);
            provider = (Provider)con.newInstance(pkcs11Config);
          }
        }
        pkcs11Initialized = true; // we have set final fields in the PKCS11 Provider - can not reconfigure
        Security.addProvider(provider);
      }

      int        old      = certInfo.size();
      KeyStore   ks       = KeyStore.getInstance("PKCS11", provider);
      ks.load(new PasswdFromGUI("Enter password for PKCS11 provider ("+pkcs11Provider+")"));
      KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
      kmf.init(ks, null);

      findCertificates(certInfo, kmf, ks, null, Shell.isWindows());

      TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
      tmf.init(ks);
      trustMgrs.addAll(Arrays.asList(tmf.getTrustManagers()));

      if (ks!=null) currentKeyStore = ks; // Since 3.9.2 added for external use of loaded keystore
      return certInfo.size() - old;
    }
    catch (Exception e) {
      LogUtilities.log(SSLUtilities.class, midas, Level.SEVERE,
          "tryInitPKCS11: Error initializing SSL Context based on PKCS11 content provider", e);
      return -1;
    }
  }

  /** Does the provided cert match a valid Digital Signature?
   *  @param cert The cert (not null).
   *  @return true if valid, false if invalid.
   *  @since NeXtMidas 3.7.3
   */
  @InternalUseOnly
  public static boolean isValidDigitalSignature (X509Certificate cert) {

    boolean[] keyUsage = cert.getKeyUsage();

    if (keyUsage == null   ) return false;
    if (keyUsage.length < 9) return false; // <-- this should be impossible, but check anyhow

    try {
      cert.checkValidity();
    }
    catch (CertificateExpiredException | CertificateNotYetValidException e) {
      return false; // Cert is not valid at this time, just skip it
    }

    // Some documentation said that this check is the proper one, but I do not
    // think that is accurate (J. Czechowski)
    //return ( keyUsage[0] && // digitalSignature
    //        !keyUsage[1] && // nonRepudiation
    //        !keyUsage[2] && // keyEncipherment
    //        !keyUsage[3] && // dataEncipherment
    //        !keyUsage[4] && // keyAgreement
    //        !keyUsage[5] && // keyCertSign
    //        !keyUsage[6] && // cRLSign
    //        !keyUsage[7] && // encipherOnly
    //        !keyUsage[8]);  // decipherOnly
    return  keyUsage[0]; // digitalSignature
  }

  /** Gets the {@link KeyStoreSpi} for a given {@link KeyStore}.
   *  @param ks The key store.
   *  @return The key store SPI.
   *  @throws ReflectiveOperationException If there is a reflection error.
   *  @since NeXtMidas 3.7.3
   */
  @InternalUseOnly
  public static KeyStoreSpi getKeyStoreSpi (KeyStore ks) throws ReflectiveOperationException {
    // This is not the ideal way of doing this, but many examples show this since there really isn't
    // a good alternative for obtaining the KeyStoreSpi in situations where it is needed.
    Field _keyStoreSpi = ks.getClass().getDeclaredField("keyStoreSpi");
    _keyStoreSpi.setAccessible(true);
    KeyStoreSpi keyStoreSpi = (KeyStoreSpi)_keyStoreSpi.get(ks);

    // Java 8 adds KeyStoreDelegator
    try {
      // In some situations (such as loading a PKCS12 but using a JKS KeyStore) the actual
      // KeyStoreSpi will be nested below a KeyStoreDelegator.
      Class<?> keyStoreDelegator = Class.forName("sun.security.provider.KeyStoreDelegator");
      if (keyStoreDelegator.isInstance(keyStoreSpi)) {
        Field keystore = keyStoreDelegator.getDeclaredField("keystore");
        keystore.setAccessible(true);

        KeyStoreSpi delegate = (KeyStoreSpi)keystore.get(keyStoreSpi);
        if (delegate != null) {
          keyStoreSpi = delegate;
        }
      }
    }
    catch (ReflectiveOperationException e) {
      LogUtilities.log(SSLUtilities.class, midas, certInfoLogLevel, "Issue accessing the KeyStoreSPI",e);
    }

    return keyStoreSpi;
  }

  /** Finds the certificates in the key store with an alias. If an alias is found that does not have
   *  the key's hash associated, it will modify the key to include it.
   *  @param certInfo  (OUT) The cert info.
   *  @param kmf       (IN)  The key manager factory
   *  @param ks        (IN)  The key store.
   *  @param passwd    (IN)  The password for the key (null if n/a)
   *  @param checkCapi (IN)  Is this Windows and capiEnabled
   *  @throws Exception If there is an error in accessing the key's information.
   *  @since NeXtMidas 3.7.3
   */
  @SuppressWarnings("unchecked")
  @InternalUseOnly
  public static void findCertificates (List<CertInfo> certInfo, KeyManagerFactory kmf,
                                        KeyStore ks, char[] passwd, boolean checkCapi)
                                       throws Exception {
    // Note 1: We need the alias to be unique within our list. Since most default to "Digital
    //         Signature", we substitute a modified version that is adds a hash of the cert plus
    //         a one-up counter (although a one-up counter would be sufficient, it was found to
    //         be confusing to users, particularly after sorting and filtering which causes the
    //         increment to be out-of-sequence, suggesting an error where none exists). We tried
    //         using the cert's serial number as a unique ID, but ran into issues with some
    //         signers using extremely long IDs (it is a BigInteger) which caused the name to
    //         exceed what we could reasonably display in a pop-up to users.
    //
    // Note 2: Large sections of this method (in particular the part about getting the private
    //         content from within a "KeyStore$MY" instance) is inspired on a set of examples I
    //         was provided. Those examples, on their own, did not work properly within a TLSv1.2
    //         environment so they are best described as "inspirational" rather than "sample code".
    //
    //         If there is a better way of doing this (i.e. without relying on access to private
    //         fields in a class that is not part of the formal Java API) it is worth considering
    //         as a replacement for what is here.
    KeyStoreSpi keyStoreSpi = (checkCapi) ? getKeyStoreSpi(ks) : null;

    if (checkCapi && keyStoreSpi.getClass().getName().equals("sun.security.mscapi.KeyStore$MY")) {
      Field _entries = keyStoreSpi.getClass().getEnclosingClass().getDeclaredField("entries");
      _entries.setAccessible(true);

      Object keyStoreSpiEntry = _entries.get(keyStoreSpi);

      if (keyStoreSpiEntry instanceof Iterable<?>) {
        for (Object entry : (Iterable<?>)keyStoreSpiEntry) {
          Field _certChain  = entry.getClass().getDeclaredField("certChain");
          Field _privateKey = entry.getClass().getDeclaredField("privateKey");
          Field _alias      = entry.getClass().getDeclaredField("alias");
          _certChain.setAccessible(true);
          _privateKey.setAccessible(true);
          _alias.setAccessible(true);

          X509Certificate[] certChain  = (X509Certificate[])_certChain.get(entry);
          PrivateKey        privateKey = (PrivateKey)_privateKey.get(entry);
          X509Certificate   cert       = certChain[0];
          LogUtilities.log(SSLUtilities.class, midas, certInfoLogLevel,
              "findCertificates if");
          if (isValidDigitalSignature(cert)) {
            String newAlias = getUniqueAlias(certInfo, cert);
            certInfo.add(new CertInfo(kmf, newAlias, cert, certChain, privateKey));
          }
        }
      }
    }
    else {
      Enumeration<String> aliases = ks.aliases();

      while (aliases.hasMoreElements()) {
        String      alias       = aliases.nextElement();
        Certificate certificate = ks.getCertificate(alias);

        if (certificate instanceof X509Certificate) {
          X509Certificate cert = (X509Certificate)certificate;
          LogUtilities.log(SSLUtilities.class, midas, certInfoLogLevel,
              "findCertificates else valid:{0}", isValidDigitalSignature(cert));

          if (isValidDigitalSignature(cert)) {
            PrivateKey    privateKey = (PrivateKey)ks.getKey(alias, passwd);
            Certificate[] certChain  = ks.getCertificateChain(alias);
            String        newAlias   = getUniqueAlias(certInfo, cert);
            if ((certChain == null) || (privateKey == null)) continue; // to prevent NPE (3.9.1 added cert chain, 3.9.3 added privateKey)

            certInfo.add(new CertInfo(kmf, newAlias, cert, certChain, privateKey));
          }
        }
      }
    }
  }

  /** Generates a unique alias for a certificate.
   *  @param certInfo The list of cert info entries.
   *  @param cert     The new cert to add with a unique alias.
   *  @return The unique alias to use.
   */
  private static String getUniqueAlias (List<CertInfo> certInfo, X509Certificate cert) {
    String subjectDN = cert.getSubjectX500Principal().getName();
    int    hash      = cert.hashCode();
    String alias     = subjectDN+" ["+Integer.toHexString(hash)+"]";

    while (getCertInfo(certInfo, alias) != null) {
      hash++;
      alias = subjectDN+" ["+Integer.toHexString(hash)+"]";
    }
    return alias;
  }

  /** Gets a CertInfo entry based on its alias.
   *  @param certInfo The list of cert info entries.
   *  @param alias    The desired alias.
   *  @return The cert if found, null otherwise.
   */
  private static CertInfo getCertInfo (List<CertInfo> certInfo, String alias) {
    for (CertInfo c : certInfo) {
      if (c.alias.equals(alias)) return c;
    }
    return null;
  }

  /** This will sign a JAR file using the certificates loaded with {@code initSSLContext(..)}. <br>
   *  <br>
   *  <b>Warning:</b> At the end of the signing process the original JAR file will be re-written
   *  with a signed version of the JAR. If signing fails, a temp file may be left behind in the
   *  directory with the
   *  @param fname The name of the JAR file to be signed.
   *  @throws IllegalStateException If the SSL context has not been initialized yet or if it did
   *                                not result in the loading of any certificates.
   */
  @SuppressWarnings("all")
  public static void signJarFile (Object fname) {
    checkInit();
    if (allCertInfo.isEmpty()) throw new IllegalStateException("No certs loaded during init.");

    // ==== INIT JAR FILE NAMES ====================================================================
    String inFileName;
    String outFileName;
    String oldFileName;

    try {
      inFileName  = FileName.toFileName(fname).toLocalFileName();
      outFileName = inFileName + ".part";
      oldFileName = inFileName + ".orig";
    }
    catch (MalformedURLException e) {
      throw new MidasException("Unable to access "+fname, e);
    }
    File inFilePath  = new File(inFileName);
    File outFilePath = new File(outFileName);
    File oldFilePath = new File(oldFileName); // <-- keep for debugging

    try {  // the extra try/catch was added for 38x to support Java 7-9 and should be removed in 3.9.0
      // ==== SIGN JAR FILE ==========================================================================
      try (JarFile inFile  = new JarFile(null, inFileName);
           JarFile outFile = new JarFile(null, inFile.getURL()+".part")) {
        inFile.open(JarFile.INPUT);
        outFile.open(JarFile.OUTPUT);

        CertInfo                 certInfo      = allCertInfo.get(0);
        String                   alias         = certInfo.getUserAlias(true);
        byte[]                   buf           = new byte[32768];
        URI                      tsaURI        = null;
        String                   tsaPolicy     = null;
        String                   keyAlgo       = certInfo.privateKey.getAlgorithm();
        boolean                  keyAlgoDSA    = keyAlgo.toUpperCase().endsWith("DSA");
        String                   dgstAlgo      = "SHA-256";
        String                   sigAlgo       = (keyAlgoDSA)? "SHA256withDSA" : "SHA256withRSA";
        Signature                sig           = Signature.getInstance(sigAlgo);
        MessageDigest            digest        = MessageDigest.getInstance(dgstAlgo);
        Manifest                 manifest      = inFile.manifest;
        Map<String,Attributes>   entries       = manifest.getEntries();
        String                   creator       = System.getProperty("java.version")
                                               + " ("+System.getProperty("java.vendor")+")";
        String                   mfFileName    = "META-INF/MANIFEST.MF";
        String                   sfFileName    = "META-INF/"+alias+".SF";
        String                   sigFileName   = "META-INF/"+alias+((keyAlgoDSA)? ".DSA" : ".RSA");

        java.util.Base64.Encoder encoder       = java.util.Base64.getEncoder();  // <-- For Java 8+ (Since 3.9.0)

        // ==== GET ENTRIES FROM MANIFEST PRUNING AS NEEDED ==========================================
        if (entries.isEmpty()) {
          Iterator<String> iter = entries.keySet().iterator();
          while (iter.hasNext()) {
            if (inFile.zipFile.getEntry(iter.next()) == null) {
              iter.remove(); // Remove "dead" entry
            }
          }
        }
        else {
          // Add some defaults...
          Attributes attr = manifest.getMainAttributes();
          attr.putValue("Manifest-Version", "1.0");
          attr.putValue("Created-By",        creator);
        }

        // I have no idea why this needs to be removed, but if you keep it in (which may come from
        // it being automatically inserted) it currently causes the verification to fail.
        manifest.getMainAttributes().remove(new Attributes.Name("Created-By"));

        // ==== UPDATE MANIFEST ======================================================================
        for (Object entry : inFile.getEntries()) {
          if (entry instanceof JarEntry) {
            JarEntry je = (JarEntry)entry;

            if (!je.isDirectory() && !je.getName().startsWith("META-INF/")) {
              FileName fileName = new FileName("jar:"+inFile.getURL()+"!/"+je.getName(), true);

              try (BaseFile bf = new BaseFile(null, fileName)) {
                bf.open();
                try (InputStream in = bf.getResource().getInputStream()) {
                  int numRead;

                  while ((numRead = in.read(buf)) >= 0) {
                    digest.update(buf, 0, numRead);
                  }

                  String dgst = encoder.encodeToString(digest.digest()); // Since 3.9.0

                  Attributes attr = entries.get(je.getName());
                  if (attr == null) {
                    attr = new Attributes();
                    entries.put(je.getName(), attr);
                  }
                  attr.putValue(dgstAlgo+"-Digest", dgst);
                }
                catch (IOException e) {
                  throw new MidasException("Error reading "+fileName, e);
                }
              }
            }
          }
          else {
            throw new MidasException("Expected JarEntry but found "+entry.getClass()+" in "+inFile.getName());
          }
        }


        // ==== SERALIZE UPDATED MANIFEST ============================================================
        byte[] manifestBuf;
        try (ByteArrayOutputStream out = new ByteArrayOutputStream(32768)) {
          manifest.write(out);
          out.flush();
          out.close();
          manifestBuf = out.toByteArray();
        }
        catch (IOException e) {
          throw new AssertionError(e); // this should be impossible
        }

        // ==== CREATE SIGNATURE FILE (*.SF) =========================================================
        Manifest               sfManifest   = new Manifest();
        Attributes             sfMainAttr   = sfManifest.getMainAttributes();
        Map<String,Attributes> sfEntries    = sfManifest.getEntries();
        Object                 mfDigester   = ManifestDigester_new(manifestBuf);
        Object                 mainAttr     = ManifestDigester_get(mfDigester, "Manifest-Main-Attributes", false);
        byte[]                 manifestDig  = ManifestDigester_manifestDigest(mfDigester, digest);
        byte[]                 mainAttrDig  = ManifestDigester_Entry_digest(mainAttr, digest);
        String                 manifestDgst = encoder.encodeToString(manifestDig);
        String                 mainAttrDgst = encoder.encodeToString(mainAttrDig);

        sfMainAttr.putValue("Signature-Version",                         "1.0");
        sfMainAttr.putValue(dgstAlgo+"-Digest-Manifest-Main-Attributes", mainAttrDgst);
        sfMainAttr.putValue(dgstAlgo+"-Digest-Manifest",                 manifestDgst);
        sfMainAttr.putValue("Created-By",                                creator);

        for (String key : manifest.getEntries().keySet()) {
          Object     attr = ManifestDigester_get(mfDigester, key, false);
          byte[]     dig  = ManifestDigester_Entry_digest(attr, digest);
          String     dgst = encoder.encodeToString(dig);
          Attributes a    = new Attributes();

          a.putValue(dgstAlgo+"-Digest", dgst);
          sfEntries.put(key, a);
        }

        // ==== SERIALIZE SIGNATURE FILE =============================================================
        byte[] sfManifestBuf;
        try (ByteArrayOutputStream out = new ByteArrayOutputStream(32768)) {
          sfManifest.write(out);
          out.flush();
          out.close();
          sfManifestBuf = out.toByteArray();
        }
        catch (IOException e) {
          throw new AssertionError(e); // this should be impossible
        }

        // ==== SIGN THE CONTENT =====================================================================
        sig.initSign(certInfo.privateKey);
        sig.update(sfManifestBuf);

        byte[] signature = sig.sign();

        byte[] pkcs7 = null;
        if (javaMajVer > 21) {
          pkcs7 = PKCS7_generateSignedData(certInfo.certChain, sfManifestBuf,
              sigAlgo, "SHA-256",certInfo.privateKey);
        }
        else {
          pkcs7 = PKCS7_generateSignedData(signature, certInfo.certChain, sfManifestBuf,
              sigAlgo, tsaURI, tsaPolicy, "SHA-256");
        }

        // ==== CREATE THE JAR FILE ==================================================================
        try (FileOutputStream     dest = new FileOutputStream(outFileName);
             BufferedOutputStream bos  = new BufferedOutputStream(dest);
             ZipOutputStream      out  = new ZipOutputStream(bos)) {

          // ---- Write the special files first ------------------------------------------------------
          out.putNextEntry(new JarEntry(mfFileName ));
          out.write(manifestBuf);
          out.closeEntry();
          out.putNextEntry(new JarEntry(sfFileName ));
          out.write(sfManifestBuf);
          out.closeEntry();
          out.putNextEntry(new JarEntry(sigFileName));
          out.write(pkcs7);
          out.closeEntry();

          // ---- Write the remaining files out ------------------------------------------------------
          for (Object entry : inFile.getEntries()) {
            JarEntry je = (JarEntry)entry;

            if (je.getName().equals(mfFileName )) continue;
            if (je.getName().equals(sfFileName )) continue;
            if (je.getName().equals(sigFileName)) continue;

            // Need to create new entry to avoid issues when there is a zero-length file in the JAR.
            out.putNextEntry(new JarEntry(je.getName()));

            if (!je.isDirectory()) {
              FileName fileName = new FileName("jar:"+inFile.getURL()+"!/"+je.getName(), true);

              try (BaseFile bf = new BaseFile(null, fileName)) {
                bf.open();
                try (InputStream in = bf.getResource().getInputStream()) {
                  int numRead;

                  while ((numRead = in.read(buf)) >= 0) {
                    out.write(buf, 0, numRead);
                  }
                  out.write(buf, 0, 0);
                }
                catch (IOException e) {
                  throw new MidasException("Error reading "+fileName, e);
                }
              }
            }
            out.closeEntry();
          }
        }
        catch (IOException e) {
          throw new MidasException("Could not write "+outFileName, e);
        }

        // ==== RENAME THE FILE(S) ===================================================================
        //inFilePath.renameTo(oldFilePath); // <-- Keep for debugging
        outFilePath.renameTo(inFilePath);
      }
      catch (InvalidKeyException|SignatureException e) {
        throw new MidasException("Could not sign JAR file", e);
      }
      catch (NoSuchAlgorithmException e) {
        // This should be impossible since we are using the standard algorithms.
        throw new AssertionError("Could not find SHA256withRSA", e);
      }
    }
    catch (IllegalArgumentException e) {
      // this catch block and its matching try block were added for Java 7-9 support in the 3.8.x series and will be removed in 3.9.0
      throw new MidasException("Reflection failure in signing JAR file", e);
    }
    finally {
      outFilePath.delete();
    }
  }

  /**
   * Set Log level for information such as certificate details
   * Allows level to be adjusted programmatically during debug
   * @param logLevel log Level
   */
  @InternalUseOnly
  public static void setCertInfoLogLevel (Level logLevel) {
    SSLUtilities.certInfoLogLevel = logLevel;
  }

  /**
   * Get Log level for information such as certificate details
   * @return logging level
   */
  @InternalUseOnly
  public static Level getCertInfoLogLevel () {
    return SSLUtilities.certInfoLogLevel;
  }

  /** Calls the {@code sun.security.pkcs.PKCS7.generateSignedData(..)} method.
   *  @param signature   The signature (not null).
   *  @param certChain   The cert chain (must have at least 1 entry).
   *  @param content     The content (not null).
   *  @param sigAlgo     The signature algorithm.
   *  @param tsaURI      The TSA URI (or null if not used)
   *  @param tsaPolicy   The TSA policy to use (n/a if tsaURI is null).
   *  @param algorithm   The algorithm to use (usually "SHA-256").
   *  @return The signed data.
   */
  private static byte[] PKCS7_generateSignedData (byte[] signature, X509Certificate[] certChain,
                                                  byte[] content, String sigAlgo,
                                                  URI tsaURI, String tsaPolicy, String algorithm) {
    try {
      // Until there is a public API for generating PKCS7 signed data, we will need to use
      // the internal PKCS7 class.
      Class<?> _PKCS7 = Class.forName("sun.security.pkcs.PKCS7");
      Method   method = _PKCS7.getMethod("generateSignedData",
                                         byte[].class, X509Certificate[].class,
                                         byte[].class, String.class, URI.class,
                                         String.class, String.class);
      return (byte[])method.invoke(null, signature, certChain, content,
                                   sigAlgo, tsaURI, tsaPolicy, algorithm);

    }
    catch (ReflectiveOperationException e) {
      throw new RuntimeException("Could not sign JAR file", e);
    }
  }

  /** Calls the {@code sun.security.pkcs.PKCS7.generateSignedData(..)} method.
   *  @param certChain   The cert chain (must have at least 1 entry).
   *  @param content     The content (not null).
   *  @param sigAlgo     The signature algorithm.
   *  @param algorithm   The algorithm to use (usually "SHA-256").
   *  @return The signed data.
   */
  private static byte[] PKCS7_generateSignedData (X509Certificate[] certChain,
                                                  byte[] content, String sigAlgo,
                                                  String algorithm, PrivateKey privateKey) {
    try {
      // Until there is a public API for generating PKCS7 signed data, we will need to use
      // the internal PKCS7 class.
      Class<?> _PKCS7 = Class.forName("sun.security.pkcs.PKCS7");
      Method   method = _PKCS7.getMethod("generateSignedData",
                                         String.class, Provider.class,PrivateKey.class,X509Certificate[].class,
                                         byte[].class, boolean.class, boolean.class, Function.class);
      return (byte[])method.invoke(null, sigAlgo, null, privateKey, certChain, content, false, false, null);
    }
    catch (ReflectiveOperationException e) {
      throw new RuntimeException("Could not sign JAR file", e);
    }
  }

  /** Creates a new {@code sun.security.util.ManifestDigester}.
   *  @param manifestBuf The manifest buffer.
   *  @return The new instance.
   */
  private static Object ManifestDigester_new (byte[] manifestBuf) {
    try {
      Class<?>       clazz = Class.forName("sun.security.util.ManifestDigester");
      Constructor<?> con   = clazz.getConstructor(byte[].class);
      return con.newInstance((Object)manifestBuf);
    }
    catch (ReflectiveOperationException e) {
      throw new RuntimeException("Can't compute manifest digest", e);
    }
  }

  /** Calls {@code get} on an instance of {@code sun.security.util.ManifestDigester}.
   *  @param manifestDigester The instance to use.
   *  @param key              The key.
   *  @param flag             The flag.
   *  @return The result.
   */
  private static Object ManifestDigester_get (Object manifestDigester, String key, boolean flag) {
    try {
      Class<?> clazz  = Class.forName("sun.security.util.ManifestDigester");
      Method   method = clazz.getDeclaredMethod("get", String.class, Boolean.TYPE);
      return method.invoke(manifestDigester, key, flag);
    }
    catch (ReflectiveOperationException e) {
      throw new RuntimeException("Can't compute manifest digest", e);
    }
  }

  /** Calls {@code manifestDigest} on an instance of {@code sun.security.util.ManifestDigester}.
   *  @param manifestDigester The instance to use.
   *  @param md               The message digest.
   *  @return The result.
   */
  private static byte[] ManifestDigester_manifestDigest (Object manifestDigester, MessageDigest md) {
    try {
      Class<?> clazz  = Class.forName("sun.security.util.ManifestDigester");
      Method   method = clazz.getDeclaredMethod("manifestDigest", MessageDigest.class);
      return (byte[])method.invoke(manifestDigester, md);
    }
    catch (ReflectiveOperationException e) {
      throw new RuntimeException("Can't compute manifest digest", e);
    }
  }

  /** Calls {@code manifestDigest} on an instance of {@code sun.security.util.ManifestDigester.Entry}.
   *  @param entry The instance to use.
   *  @param md    The message digest.
   *  @return The result.
   */
  private static byte[] ManifestDigester_Entry_digest (Object entry, MessageDigest md) {
    try {
      Class<?> clazz  = Class.forName("sun.security.util.ManifestDigester$Entry");
      Method   method = clazz.getDeclaredMethod("digest", MessageDigest.class);
      return (byte[])method.invoke(entry, md);
    }
    catch (ReflectiveOperationException e) {
      throw new RuntimeException("Can't compute manifest digest", e);
    }
  }


  /** Set User Query
   *  @param userQuery base url for user query
   */
  @InternalUseOnly
  public static void setUserQuery (String userQuery) {
    SSLUtilities.userQuery = userQuery;
  }

  /** Set Group Query
   *  @param groupQuery base url for group query
   */
  @InternalUseOnly
  public static void setGroupQuery (String groupQuery) {
    SSLUtilities.groupQuery = groupQuery;
  }


  /** Add to Json Key Path - designed for INSECURE testing
   *  @param jsonKey JSON key (value being searched for)
   *  @param jsonPath    path in JSON to reach that value
   */
  @InternalUseOnly
  public static void addToJsonKeyPath (String jsonKey, String jsonPath) {
    jsonKeyPaths.put(jsonKey.toLowerCase(), jsonPath);
  }

  /** Get the current KeyStore
   *  @return the currentKeyStore
   *  @since 3.9.1
   */
  @ProvisionalUseOnly("Use with caution")
  public static KeyStore getCurrentKeyStore () {
    return currentKeyStore;
  }

  /** Get the current TrustStore
   *  @return the currentTrustStore
   *  @since 3.9.1
   */
  @ProvisionalUseOnly("Use with caution")
  public static KeyStore getCurrentTrustStore () {
    return currentTrustStore;
  }

  /** Set the NSS Library Location
   *  @param nssLibsLoc the NSS Library Location
   */
  public static void setNssLibsDir (String nssLibsLoc) {
    SSLUtilities.nssLibsDir = nssLibsLoc;
  }

  /** Get the CertInfo for all PKI Certs found
   *
   *  @return List with CertInfo for each certificate
   */
  @ProvisionalUseOnly("Read Only")
  public static List<CertInfo> getAllCertInfo () {
    return SSLUtilities.allCertInfo;
  }

//******* Inner classes below this point ************************//

  /** A map that auto-generates a unique key for each put and maintains insertion order (a feature
   *  in Java 8 but not Java 7). The unique key will be in the form {@code "<key>\t[<id>]"} where
   *  {@code <id>} is a unique ID. If the {@code "\t"} token is seen it is assumed to map to one
   *  of the keys that has already been made unique. This class makes the assumption that none of
   *  the keys provided will contain a {@code "\t"} token.
   *  @param <K> The key type (must be String).
   *  @param <V> The value type.
   *  @since NeXtMidas 3.7.3
   */
  @SuppressWarnings({"element-type-mismatch","unchecked","CollectionWithoutInitialCapacity",
                     "EqualsWhichDoesntCheckParameterClass","UseOfObsoleteCollectionType"})
  private static class EntriesMap<K,V> implements Map<K,V> {

    private final Map<K,V> map   = Collections.synchronizedMap(new LinkedHashMap<>());
    private       int      count = 0;

    @Override public int                 size          ()         { return map.size();           }
    @Override public boolean             isEmpty       ()         { return map.isEmpty();        }
    @Override public boolean             containsValue (Object v) { return map.containsValue(v); }
    @Override public boolean             containsKey   (Object k) { return map.containsKey(k);   }
    @Override public V                   get           (Object k) { return map.get(k);           }
    @Override public V                   remove        (Object k) { return map.remove(k);        }
    @Override public void                clear         ()         {        map.clear();          }
    @Override public String              toString      ()         { return map.toString();       }
    @Override public Set<K>              keySet        ()         { return map.keySet();         }
    @Override public Set<Map.Entry<K,V>> entrySet      ()         { return map.entrySet();       }
    @Override public Collection<V>       values        ()         { return map.values();         }
    @Override public boolean             equals        (Object o) { return map.equals(o);        }
    @Override public int                 hashCode      ()         { return map.hashCode();       }

    @Override
    public void putAll (Map<? extends K, ? extends V> t) {
      map.putAll(t);
    }

    @Override
    public V put (K key, V value) {
      if (key instanceof String) { // <-- This should always be true
        int index = key.toString().indexOf('\t');
        if (index < 0) {
          key = (K)(key+"\t["+(count++)+"]");
        }
      }
      return map.put(key, value);
    }

  }

  /** A filter to apply to the certificates found. This could be used within an application to
   *  prompt a user to select the applicable cert and then returning a singleton list or it
   *  could be used to auto-select based on certificate information (e.g. filtering based on
   *  whether the certificate has an email address within the corporate domain).
   *  @since NeXtMidas 3.7.3
   */
  public static interface CertFilter {
    /** Filters the provided certificates.
     *  @param certs All of the certificates found (not null).
     *  @return All of the certificates that pass the filter in priority order. This can be
     *          empty, but can not be null.
     *  @since NeXtMidas 3.7.3
     */
    List<CertInfo> filter (List<CertInfo> certs);
  }

  /** Details about a particular certificate.
   *  @since NeXtMidas 3.7.3
   */
  public final static class CertInfo {
    final KeyManagerFactory kmf;
    final String            alias;
    final X509Certificate   cert;
    final X509Certificate[] certChain;
    final PrivateKey        privateKey;

    /** Creates a new instance.
     *  @param kmf        The key manager factory.
     *  @param alias      The certificate alias.
     *  @param cert       The certificate.
     *  @param certChain  The certificate chain.
     *  @param privateKey The private key.
     *  @since NeXtMidas 3.7.3
     */
    CertInfo (KeyManagerFactory kmf, String alias, X509Certificate cert,
              X509Certificate[] certChain, PrivateKey privateKey) {
      this.kmf        = requireNonNull(kmf);
      this.alias      = requireNonNull(alias);
      this.cert       = requireNonNull(cert);
      this.certChain  = requireNonNull(certChain);
      this.privateKey = requireNonNull(privateKey);
    }

    /** Creates a new instance.
     *  @param kmf        The key manager factory.
     *  @param alias      The certificate alias.
     *  @param cert       The certificate.
     *  @param certChain  The certificate chain.
     *  @param privateKey The private key.
     *  @since NeXtMidas 3.7.3
     */
    CertInfo (KeyManagerFactory kmf, String alias, X509Certificate cert,
              Certificate[] certChain, PrivateKey privateKey) {
      this(kmf, alias, cert, new X509Certificate[certChain.length], privateKey);

      for (int i = 0; i < certChain.length; i++) {
        this.certChain[i] = (X509Certificate)certChain[i];
      }
    }

    /** The alias associated with the certificate.
     *  @return The alias associated with the certificate.
     *  @since NeXtMidas 3.7.3
     */
    @Override
    public String toString () {
      return getAlias();
    }

    /** The alias associated with the certificate.
     *  @return The alias associated with the certificate.
     *  @since NeXtMidas 3.7.3
     */
    public String getAlias () {
      return alias;
    }

    /** The certificate.
     *  @return The certificate.
     *  @since NeXtMidas 3.7.3
     */
    public X509Certificate getX509Certificate () {
      return cert;
    }

    /** The user's distinguished name.
     *  @return The user's distinguished name.
     *  @since NeXtMidas 3.7.3
     */
    public String getUserDN () {
      return getX509Certificate().getSubjectX500Principal().getName();
    }

    /** The user's common name.
     *  @return The user's common name. If it can't be found, the user's distinguished name will
     *          be returned.
     *  @since NeXtMidas 3.7.3
     */
    public String getUserCN () {
      return getUserCNAlias(false);
    }

    /** The user's common name alias. This will look for the user's common name and will then
     *  parse it to try and find something that looks like a host name or a user's SID. The
     *  normalized form matches the alias used with JAR signing which is all-caps, limited to
     *  8 characters and with no special characters.
     *  <pre>
     *    +----------------------------+---------------------------------+------------------+
     *    |       Common Name          |              Alias              | Normalized Alias |
     *    +----------------------------+---------------------------------+------------------+
     *    | myhost.example.com         | myhost                          | MYHOST           |
     *    +----------------------------+---------------------------------+------------------+
     *    | another-host.example.com   | another-host                    | ANOTHERH         |
     *    +----------------------------+---------------------------------+------------------+
     *    | John Q. PUBLIC             | PUBLIC                          | PUBLIC           |
     *    +----------------------------+---------------------------------+------------------+
     *    | PUBLIC John Q.             | PUBLIC                          | PUBLIC           |
     *    +----------------------------+---------------------------------+------------------+
     *    | John Q. Public             | John                            | JOHN             |
     *    +----------------------------+---------------------------------+------------------+
     *    | John Q. Public jqpublic    | jqpublic                        | JQPUBLIC         |
     *    +----------------------------+---------------------------------+------------------+
     *    | John Q. Public jqpub123    | jqpub123                        | JQPUB123         |
     *    +----------------------------+---------------------------------+------------------+
     *  </pre>
     *  @param normalize Normalize the alias?
     *  @return The user's common alias. If it can't be determined, the user's common name (or
     *          distinguished name) will be returned.
     *  @since NeXtMidas 3.7.3
     */
    public String getUserAlias (boolean normalize) {
      String userAlias = getUserCNAlias(true);
      return (normalize)? normalizeAlias(userAlias) : userAlias;
    }

    /** The user's common name or alias.
     *  @param alias Get the alias.
     *  @return The user's common name.
     *  @since NeXtMidas 3.7.3
     */
    private String getUserCNAlias (boolean alias) {
      String userDN = getUserDN();
      int    start  = userDN.toLowerCase().indexOf("cn=");
      if (start < 0) return userDN;

      int end = userDN.indexOf(',',start);
      if (end < 0) end = userDN.length();

      String userCN = userDN.substring(start+3, end);
      return (alias)? toAlias(userCN) : userCN;
    }

    /** Gets a "normalized" version of the user alias.
     *  @return The normalized user alias.
     */
    String normalizeAlias (String alias) {
      for (int i = 0; i < alias.length(); i++) {
        char c = alias.charAt(i);
        if (!Character.isAlphabetic(c) && !Character.isDigit(c) && (c != '-') && (c != '_')) {
          alias = alias.substring(0,i)+'_'+alias.substring(i+1);
        }
      }
      if (alias.length() > 8) alias = alias.substring(0,8);
      return alias.toUpperCase();
    }

    /** Converts to an alias.
     *  @param userCN The user common name.
     *  @return The alias.
     */
    private String toAlias (String userCN) {
      if (userCN.indexOf(' ') < 0) {
        int index = userCN.indexOf('.');
        if ((index >= 1) && Character.isAlphabetic(userCN.charAt(0))) {
          // Probably a host name, return the first part of the name
          return userCN.substring(0,index);
        }
        else {
          // Only one "name" in the CN, so this must be it
          return userCN;
        }
      }
      else {
        // Multiple "names" in the CN, try to find the best one. This considers the best
        // name to be something that looks like a user SID (has numbers) or is in all
        // lower-case (with numbers having priority). Failing that return the first one
        // found.
        String names[]  = userCN.split("[ ]+");
        String bestName = names[0];
        int    bestRank = 0;

        for (String name : names) {
          boolean allLower   = true;
          boolean allUpper   = true;
          boolean anyNumbers = false;
          for (int j = 0; j < name.length(); j++) {
            char c = name.charAt(j);
            allLower   = allLower   && Character.isLowerCase(c);
            allUpper   = allUpper   && Character.isUpperCase(c);
            anyNumbers = anyNumbers || Character.isDigit(c);
          }
          int rank = 0;

               if (anyNumbers) rank = 100;
          else if (allLower  ) rank =  10;
          else if (allUpper  ) rank =   5;

          if (rank > bestRank) {
            bestName = name;
            bestRank = rank;
          }
        }
        return bestName;
      }
    }
  }

  /** An extension of {@link X509ExtendedKeyManager} that uses the provided alias.
   *  @since NeXtMidas 3.7.3
   */
  private static class CertKeyManager extends X509ExtendedKeyManager {
    private final X509KeyManager mgr;
    private final List<CertInfo> certInfo;

    /** Creates a new instance.
     *  @param kmf      The factory to use.
     *  @param certInfo The set of certificate information to use, sorted by priority.
     *  @since NeXtMidas 3.7.3
     */
    CertKeyManager (KeyManagerFactory kmf, List<CertInfo> certInfo) {
      requireNonNull(kmf);
      requireNonNull(certInfo);

      this.mgr      = (X509KeyManager)kmf.getKeyManagers()[0];
      this.certInfo = certInfo;
    }

    @Override
    public String toString () {
      return "CertKeyManager: mgr="+mgr+" certInfo="+certInfo;
    }

    @Override
    public String chooseClientAlias (String[] s, Principal[] p, Socket k) {
      if (!certInfo.isEmpty()) {
        return certInfo.get(0).alias;
      }
      return mgr.chooseClientAlias(s,p,k);
    }

    @Override
    public String chooseEngineClientAlias (String[] s, Principal[] p, SSLEngine e) {
      if (!certInfo.isEmpty()) {
        return certInfo.get(0).alias;
      }
      if (mgr instanceof X509ExtendedKeyManager) {
        return ((X509ExtendedKeyManager)mgr).chooseEngineClientAlias(s,p,e);
      }
      return null;
    }

    @Override
    public X509Certificate[] getCertificateChain (String alias) {
      CertInfo c = getCertInfo(certInfo, alias);
      return (c != null)? c.certChain : mgr.getCertificateChain(alias);
    }

    @Override
    public PrivateKey getPrivateKey (String alias) {
      CertInfo c = getCertInfo(certInfo, alias);
      return (c != null)? c.privateKey : mgr.getPrivateKey(alias);
    }

    @Override public String   chooseServerAlias (String s, Principal[] p, Socket k) { return mgr.chooseServerAlias(s,p,k); }
    @Override public String[] getClientAliases  (String s, Principal[] p          ) { return mgr.getClientAliases(s,p);    }
    @Override public String[] getServerAliases  (String s, Principal[] p          ) { return mgr.getServerAliases(s,p);    }
  }

  /** Prompts for the password via a GUI
   *  @since NeXtMidas 3.7.3
   */
  static final class PasswdFromGUI implements CallbackHandler, LoadStoreParameter {
    private final String prompt;

    /** Creates a new instance
     * @param prompt The user prompt.  If null, the prompt passed to the handler
     *               will be used.
     */
    PasswdFromGUI (String prompt) {
      this.prompt = prompt;
    }

    @Override
    public ProtectionParameter getProtectionParameter() {
      return new CallbackHandlerProtection(this);
    }

    @Override
    public void handle (Callback[] callbacks) throws IOException,
                                                     UnsupportedCallbackException {
      for (Callback cb : callbacks) {
        if (cb instanceof PasswordCallback) {
          PasswordCallback pwdCb = (PasswordCallback)cb;
          String           ppt   = (prompt != null) ? prompt : pwdCb.getPrompt();

          pwdCb.setPassword(Shell.passwdFromGUI(ppt));  // Changed to use new graphical password popup in Shell.java since 3.9.1 for Java 11 modularity
        }
        else {
          throw new UnsupportedCallbackException(cb, "Unrecognized Callback");
        }
      }
    }
  }

  /** For Insecure SSL to avoid having to match FQDN - may get updated extended
   *  to at least check for startsWith
   *  @since NextMidas 3.9.0
   */
  @InternalUseOnly
  public static class AcceptAllHostNameVerifier implements HostnameVerifier {

    /** default constructor */
    public AcceptAllHostNameVerifier () {}

    @Override
    public boolean verify (String hostname, SSLSession session) {
      return true;
    }
  }


  /** Represents a PEM file with the certs for a user. */
  public static class PemFile {
    protected static final char[] NO_PASSWORD = new char[0];

    protected final String             fileName;
    protected final KeyStore           ks;
    protected final String             alias;
    protected final PasswordProtection pass;
    protected final PrivateKeyEntry    pvtKey;
    protected final X509Certificate    cert;

    /** Creates a new instance.
     *  @param fileName   The PEM file name.
     *  @param alias      The alias used.
     *  @param certChain  The cert chain.
     *  @param publicKey  Certificate public key
     *  @param privateKey Certificate private key
     *  @throws GeneralSecurityException If there is an issue getting the cert from the key store.
     *  @throws IOException              If there is an i/o issue getting the cert from the key store.
     */
    public PemFile (String fileName, String alias, X509Certificate[] certChain, PublicKey publicKey, PrivateKey privateKey)
            throws GeneralSecurityException, IOException {
      if ((certChain == null) || (certChain.length == 0)) {
        throw new IllegalArgumentException("Missing required certificate in "+fileName);
      }
      if (publicKey == null) {
        // In my experience (J. Czechowski), you will typically see a CERTIFICATE
        // and a PRIVATE KEY entry with no PUBLIC KEY entry.  The need for the
        // public key (below) is questionable..., but we can get it from the
        // certificate, so just do that.
        publicKey = certChain[0].getPublicKey();
      }

      requireNonNull(publicKey,  "Missing required public key in "+fileName);
      requireNonNull(privateKey, "Missing required private key in "+fileName);

      // When constructing the KeyStore, the following must be true, even
      // though they seem like non-sequiturs when applied to a PEM file:
      //   - certChain[0] must be the cert for the private key
      //   - A PasswordProtection instance must be specified and the password
      //     must be non-null for the PrivateKeyEntry
      //   - ks.load(null, null) must be called before setting entries
      //   - The value of the alias isn't particularly important, we just need
      //     something reasonably unique and identifiable
      this.fileName = fileName;
      this.alias    = alias;
      this.pass     = new PasswordProtection(NO_PASSWORD);
      this.pvtKey   = new PrivateKeyEntry(privateKey, certChain);
      this.ks       = KeyStore.getInstance("PKCS12");

      ks.load(null, null);
      ks.setEntry(alias, pvtKey, pass);

      this.cert = (X509Certificate)ks.getCertificate(alias);
    }

    /** Gets the key store.
     *  @return The key store (not null).
     */
    public KeyStore getKeyStore () {
      return ks;
    }

    /** Gets the key store.
     *  @return The key store (not null).
     */
    public String getAlias () {
      return alias;
    }

    /** Gets the private key.
     *  @return The key (not null).
     */
    public PrivateKeyEntry getPrivateKeyEntry () {
      return pvtKey;
    }

    /** Gets the password protection for the key.
     *  @return The protection (not null, but might not have a password).
     */
    public PasswordProtection getPasswordProtection () {
      return pass;
    }

    /** Gets the X509 certificate.
     *  @return The cert (not null).
     */
    public X509Certificate getCertificate () {
      return cert;
    }

    /** Gets the X500 subject.
     *  @return The cert (not null).
     */
    public X500Principal getSubject () {
      return cert.getSubjectX500Principal();
    }

    /** Gets the X500 issuer.
     *  @return The cert (not null).
     */
    public X500Principal getIssuer () {
      return cert.getIssuerX500Principal();
    }

    @Override
    public String toString () {
      // This conforms with other "<user><issuer>" uses
      return "<"+getSubject()+"><"+getIssuer()+">";
    }
  }

}
