# SSLCertGenUtil.java

    /*
    ** Module   : SSLCertGenUtil.java
    ** Abstract : Utility to generate the root CA and SSL certificates for all the accounts in a
    **            directory.
    **
    ** Copyright (c) 2014-2024, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
    ** 001 CA  20140207 First version.
    ** 002 CA  20140411 Added option to save the root CA in the server certs store too (to be used
    **                  as a trusted cert store).
    **                  Added option to save the private key for the root CA in an external key store.
    ** 003 CA  20140707 Allow the account private keys to be saved in external store(s).
    **                  Extracted generate() code into distinct methods.
    ** 004 CA  20160106 Enforce a RSA key length of at least 2048 bits.
    **                  Allow server private-keys to be saved in external key-store(s).
    ** 005 CA  20170630 Allow re-use of the encryption password.  Allow custom common name for
    **                  certificate.
    ** 006 SBI 20171029 Added named parameters and changed to rebuild certificates for the given alias.
    ** 007 SBI 20171102 Changed createEmptyStore() to have a package access.
    **     SBI 20171103 Added ACME client options to request and to load CA certificate.
    ** 008 SBI 20171109 Fixed load external certificates map, fixed to save public and private
    **                  certificates with its trusted chain in the key store.
    ** 009 SBI 20171112 Added CertificateSuite usage.
    **     SBI 20171116 Added webCertificates configuration to the directory.
    ** 010 CA  20210525 Fixed a NPE when the company details do not exist in the directory.
    **                  Log the checksum of the certificate, for each alias.
    **     CA  20210917 Added getFactory() and made createAES256BitKey() public.
    **     TJD 20220504 Java 11 compatibility minor changes
    ** 011 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
    ** 012 TJD 20240208 Java 17 dependencies updates, removed AcmeClient
    */
    /*
    ** This program is free software: you can redistribute it and/or modify
    ** it under the terms of the GNU Affero General Public License as
    ** published by the Free Software Foundation, either version 3 of the
    ** License, or (at your option) any later version.
    **
    ** This program is distributed in the hope that it will be useful,
    ** but WITHOUT ANY WARRANTY; without even the implied warranty of
    ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    ** GNU Affero General Public License for more details.
    **
    ** You may find a copy of the GNU Affero GPL version 3 at the following
    ** location: https://www.gnu.org/licenses/agpl-3.0.en.html
    **
    ** Additional terms under GNU Affero GPL version 3 section 7:
    **
    **   Under Section 7 of the GNU Affero GPL version 3, the following additional
    **   terms apply to the works covered under the License.  These additional terms
    **   are non-permissive additional terms allowed under Section 7 of the GNU
    **   Affero GPL version 3 and may not be removed by you.
    **
    **   0. Attribution Requirement.
    **
    **     You must preserve all legal notices or author attributions in the covered
    **     work or Appropriate Legal Notices displayed by works containing the covered
    **     work.  You may not remove from the covered work any author or developer
    **     credit already included within the covered work.
    **
    **   1. No License To Use Trademarks.
    **
    **     This license does not grant any license or rights to use the trademarks
    **     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
    **     of Golden Code Development Corporation. You are not authorized to use the
    **     name Golden Code, FWD, or the names of any author or contributor, for
    **     publicity purposes without written authorization.
    **
    **   2. No Misrepresentation of Affiliation.
    **
    **     You may not represent yourself as Golden Code Development Corporation or FWD.
    **
    **     You may not represent yourself for publicity purposes as associated with
    **     Golden Code Development Corporation, FWD, or any author or contributor to
    **     the covered work, without written authorization.
    **
    **   3. No Misrepresentation of Source or Origin.
    **
    **     You may not represent the covered work as solely your work.  All modified
    **     versions of the covered work must be marked in a reasonable way to make it
    **     clear that the modified work is not originating from Golden Code Development
    **     Corporation or FWD.  All modified versions must contain the notices of
    **     attribution required in this license.
    */

    package com.goldencode.p2j.security;

    import java.io.*;
    import java.math.*;
    import java.security.*;
    import java.security.cert.Certificate;
    import java.security.cert.*;
    import java.util.*;

    import com.goldencode.p2j.util.logging.*;
    import org.kohsuke.args4j.*;
    import org.kohsuke.args4j.spi.*;

    import com.goldencode.p2j.cfg.*;
    import com.goldencode.p2j.directory.*;
    import com.goldencode.p2j.security.SSLCertFactory.*;
    import com.goldencode.util.*;

    /**
     * Generate new root CA and SSL certificates for all the accounts which authenticate via a
     * certificate and save them and the associated private keys in the directory.  All existing CAs,
     * peer certificates and private keys will be removed.
     * <p>
     * The private keys will be saved under the "/security/certificates/private-keys/" node.
     * <p>
     * The company information (used to build the certificate's subject) will be saved under the
     * "/security/certificates/company/" node.  If the node does not already exist, it will ask the
     * user for the information.
     */
    public class SSLCertGenUtil
    {
       /** Constant identifying the CAs node in the directory. */
       private static final String ROOT_CA_NODE = "/security/certificates/cas/";

       /** Constant identifying the peer certs node in the directory. */
       private static final String ROOT_PEER_NODE = "/security/certificates/peers/";

       /** Constant identifying the security/certificates node in the directory. */
       private static final String ROOT_CERTIFICATES_NODE = "/security/certificates";

       /** Constant identifying the process accounts node in the directory. */
       private static final String ROOT_PROCESSES_NODE = "/security/accounts/processes/";

       /** Constant identifying the user accounts node in the directory. */
       private static final String ROOT_USERS_NODE = "/security/accounts/users/";

       /** Constant identifying the company information node in the directory. */
       private static final String ROOT_COMPANY_NODE = "/security/certificates/company/";

       /** Constant identifying the common name node in the directory. */
       private static final String ROOT_CERT_COMMON_NAME = "/security/certificates/company/hostnames/";

       /** Constant identifying the private keys container node in the directory. */
       private static final String ROOT_PRIVATE_KEYS_NODE = "/security/certificates/private-keys/";

       /** Constant identifying the name of the company node containing the certificate validity. */
       private static final String CERT_VALIDITY_YEARS_NODE = "validityYears";

       /**
        * Constant identifying the name of the company node containing the RSA private key strength.
        */
       private static final String RSA_PRIVATE_KEY_STRENGTH = "rsaKeyStrength";

       /** Constant identifying the name of the company node containing the RSA public exponent. */
       private static final String RSA_PUBLIC_EXPONENT = "rsaPublicExponent";

       /** The default value for the RSA key length */
       private static final Integer RSA_KEY_LENGTH_DEFAULT_VALUE = 2048;

       /** The default value for the RSA public exponent */
       private static final BigInteger RSA_PUBLIC_EXPONENT_DEFAULT_VALUE = new BigInteger("65537");

       /** The default server certificates JKS store */
       private static final String SERVER_CERTS_STORE = "srv-certs.store";

       /** The default root CA private keys JKS store */
       private static final String ROOT_CA_PRIVATE_KEYS_STORE = "root-ca-pk.store";

       /** Valid entries for yes/no options. */
       private static final String[] YES_NO = { "yes", "no" };

       /** Logger */
       private static final CentralLogger LOG = CentralLogger.get(SSLCertGenUtil.class);

       /** Read data from the standard input. */
       private final BufferedReader reader;

       /** Configuration used to initialize the directory. */
       private final BootstrapConfig cfg;

       /** Map identifying the company attributes. */
       private final Map<String, String> company = new HashMap<>();

       /** All the collected aliases from the defined accounts. */
       private final Set<String> aliases = new TreeSet<>();

       /** All the aliases associated with server processes. */
       private final Set<String> serverAliases = new TreeSet<>();

       /** Factory to generate the SSL certificate. */
       private final SSLCertFactory factory;

       /** Store where to add the trusted certificates (servers and CAs). */
       private final KeyStore trustCertStore;

       /** Store where to add the private keys for the trusted certificates. */
       private final KeyStore trustKeyStore;

       /** Store where to add the certificates associated with non-server accounts. */
       private final KeyStore accCertStore;

       /**
        * Store where to add the private keys for the certificates associated with non-server
        * accounts.
        */
       private final KeyStore accKeyStore;

       /**
        * The random passwords used to encrypt the private keys in their {@link KeyStore}, per each
        * alias.
        */
       private final Map<String, String> keyEntryPasswords = new HashMap<>();

       /** The certificate's validity, in years. */
       private int validity = -1;

       /** Alias for the root CA. */
       private String rootCAAlias = null;

       /** Password for the root CA (if reused). */
       private String rootCAPassword = null;

       /** Instance to access the directory. */
       private DirectoryService ds;

       /** The master password to encrypt all private keys, in the directory. */
       private String masterPassword = null;

       /** Flag indicating if passwords are reused from the directory. */
       private boolean reusePasswords = false;

       /** Flag indicating if the existing root CA is re-used. */
       private boolean reuseRootCA = false;

       /** Flag indicating if the externally generated root CA is used. */
       private boolean externalRootCA = false;

       /** The passwords used to encrypt the directory private keys. */
       private Map<String, String> directoryPasswords = new HashMap<>();

       /** Array from where to read the input. */
       private String[] inputs = null;

       /** Current index in the {@link #inputs}, if set. */
       private int currentInputIdx = 0;

       /** Current web alias */
       private String webAlias;

       /** Web private key password in the directory */
       private String webPrivateKeyPassword;

       /** Manages web certificates in the directory */
       private WebCertificates webCerts;

       /** P2J server identifier */
       private String serverId;

       /**
        * Input parameters
        */
       private InputParameters inputParameters;

       enum RequestInfo
       {
          /** Organization unit */
          OU,

          /** Organization */
          O,

          /** Locality (city) */
          L,

          /** State (province name) */
          ST,

          /** Country code */
          C
       }   /**
        * Create a new utility.
        *
        * @param    config
        *           Configuration for accessing the directory.
        *
        * @throws   SSLCertGenException
        *           If the utility could not be instantiated.
        */
       public SSLCertGenUtil(BootstrapConfig config)
       throws SSLCertGenException
       {
          Console console = System.console();

          if (console != null)
          {
             reader = new BufferedReader(console.reader());
          }
          else
          {
             reader = new BufferedReader(new InputStreamReader(System.in));
          }

          cfg = new BootstrapConfig(config);
          if (cfg.getString("directory", "xml", "filename", null) == null)
          {
             throw new NullPointerException("The directory file must be specified!");
          }

          // hard-coded to Bouncy Castle
          this.factory = getFactory();

          // we are using 4 KeyStore instances:
          // 1. key store for the private key for the self-signed, root CA certificate
          // 2. trust-store with the trusted certificates (root CA certificate + server certificates)
          // 3. key store for the private key for each non-serverprocess and user accounts
          // 4. cert store with the process and user certificates

          trustCertStore = createEmptyStore();
          trustKeyStore  = createEmptyStore();
          accCertStore  = createEmptyStore();
          accKeyStore   = createEmptyStore();
       }

       /**
        * Return a {@link SSLCertFactory} instance.
        *
        * @return   A new {@link SSLCertFactory} instance.
        *
        * @throws   SSLCertGenException
        *           If the factory could not be instantiated.
        */
       public static SSLCertFactory getFactory()
       throws SSLCertGenException
       {
          return new BCCertFactory();
       }

       /**
        * Create a random 256-bit password to be used as an AES encryption key.
        *
        * @return   See above.
        */
       public static String createAES256BitKey()
       {
          return RandomWordGenerator.create(32, 8, 4);
       }

       /**
        * Main method to (re)generate the root CA, peer certificates and private keys.
        *
        * @throws    ConfigurationException
        *            In case the {@link #ds directory service} could not be initialized.
        * @throws    IOException
        *            If standard input can not be accessed.
        * @throws    SSLCertGenException
        *            In case of problems during the generation of root CA or peer certificates.
        */
       public void generate()
       throws ConfigurationException,
              IOException,
              SSLCertGenException
       {
          cfg.setConfigItem("directory", "xml", "must_exist", "true");

          String dirFile = cfg.getString("directory", "xml", "filename", null);
          System.out.println("Initializing service for directory " + dirFile + "...");
          this.ds = DirectoryService.createInstance(cfg);

          this.webCerts = new WebCertificates(this.factory);

          if (!ds.bind())
             throw new RuntimeException("bind() failed");

          // step 1. read configuration from the /security/certificates/company node
          //         any missing nodes are read from user input and saved
          System.out.println("Reading company configuration...");
          readCompanyConfiguration();

          // step 2. read processes (servers and batch) and users configured with a alias
          readAccounts(ROOT_PROCESSES_NODE);
          readAccounts(ROOT_USERS_NODE);

          // step 3. read web certificate alias and private key password

          if (inputParameters.serverId == null || inputParameters.serverId.trim().isEmpty())
          {
             this.serverId = readLine("Enter the target P2J server identifier (by default \"standard\"): ");

             if (this.serverId.isEmpty())
             {
                this.serverId = "standard";
             }
          }
          else
          {
             this.serverId = inputParameters.serverId.trim();
          }

          webAlias = webCerts.getWebAlias(this.ds, this.serverId);

          webPrivateKeyPassword = webCerts.getPrivateKeyPassword(this.ds, this.serverId);

          String answer = null;

          if (inputParameters.externalRootCA != null)
          {
             inputParameters.reuseRootCA = BooleanOption.NO;
          }
          // check if we re-use the passwords
          if (inputParameters.reusePasswords == null)
          {
             answer = readOption("Re-use the encryption passwords currently in "
                      + "the directory (yes/no): ", YES_NO);
             reusePasswords = "yes".equals(answer);
          }
          else
          {
             reusePasswords = BooleanOption.YES.equals(inputParameters.reusePasswords);
          }

          // check if we re-use the root CA
          if (inputParameters.reuseRootCA == null)
          {
             answer = readOption("Re-use the root CA currently in the directory (yes/no): ", YES_NO);
             reuseRootCA = "yes".equals(answer);
          }
          else
          {
             reuseRootCA = BooleanOption.YES.equals(inputParameters.reuseRootCA);
          }

          if (reuseRootCA)
          {
             if (inputParameters.privateKeyPassword == null)
             {
                rootCAPassword = readLine("Enter the root CA private-key encryption password: ");
             }
             else
             {
                rootCAPassword = inputParameters.privateKeyPassword;
             }
          }
          else
          {
             // load external certificate if not reuse root CA
             if (inputParameters.externalRootCA == null)
             {
                answer = readOption("Load and use the externally generated root CA (yes/no): ", YES_NO);
                externalRootCA = "yes".equalsIgnoreCase(answer);
                if (externalRootCA)
                {
                   inputParameters.externalRootCA.put(
                            ExternalCertificates.cert,
                            readLine("Enter a root CA certificate pem file path:"));
                   inputParameters.externalRootCA.put(
                            ExternalCertificates.key,
                            readLine("Enter an associated private key pem file path:"));
                   inputParameters.externalRootCA.put(
                            ExternalCertificates.chain,
                            readLine("Enter an associated intermediate certificates"
                                     + " chain pem file path:"));
                }
             }
             else
             {
                externalRootCA = inputParameters.externalRootCA != null;
             }
          }

          boolean rebuildAliasOnly = inputParameters.buildAliasCerts != null;

          if (rebuildAliasOnly)
          {
             if (!aliases.contains(inputParameters.buildAliasCerts))
             {
                throw new SSLCertGenException("There are no accounts having this alias " +
                         inputParameters.buildAliasCerts);
             }
          }

          // step 3. generate root CA and the certificates for each account
          // generate or read CA from the directory
          generateRootCA();

          if (inputParameters.buildAliasCerts != null)
          {
             generateCertificate(inputParameters.buildAliasCerts);
          }
          else
          {
             generateCertificates();
          }

          // check how to encrypt the private keys
          if (!reusePasswords)
          {
             // use master password woks only if reusePasswords is set to no
             if (inputParameters.useMasterPassword == null)
             {
                answer = readOption("Encrypt the in-directory private keys using the same password " +
                                    "(yes/no): ", YES_NO);
                inputParameters.useMasterPassword = BooleanOption.valueOf(answer.toUpperCase());
             }

             masterPassword = (BooleanOption.YES == inputParameters.useMasterPassword) ?
                      createAES256BitKey() : null;
          }

          // step 4. save the certificates and the keys in the directory

          if (rebuildAliasOnly)
          {
             // updates the directory: private and public keys for the given alias
             saveCertificatePair(inputParameters.buildAliasCerts);
             // save private and public keys for the given alias to external key store
             saveCertificatePairInExternalStore(inputParameters.buildAliasCerts);
             System.out.println("Done.");
             return;
          }

          if (!reuseRootCA)
          {
             saveRootCA();
             saveCertificates();
          }
          else
          {
             saveCertificates();
          }

          updateWebCertificates(this.serverId);

          if (!ds.unbind())
             throw new RuntimeException("unbind() failed");

          if (inputParameters.exportAccountPrivateKeys == null)
          {
             answer = readOption("Save the account private keys in external key-store(s) (yes/no): ",
                                 YES_NO);
             inputParameters.exportAccountPrivateKeys = BooleanOption.valueOf(answer.toUpperCase());
          }

          if (inputParameters.exportAccountPrivateKeys == BooleanOption.YES)
          {
             savePrivateKeys(false);
          }

          if (inputParameters.exportServerPrivateKeys == null)
          {
             answer = readOption("Save the server private keys in external key-store(s) (yes/no): ",
                                 YES_NO);
             inputParameters.exportServerPrivateKeys = BooleanOption.valueOf(answer.toUpperCase());
          }

          if (inputParameters.exportServerPrivateKeys == BooleanOption.YES)
          {
             savePrivateKeys(true);
          }

          // save server's certificates in an external key store
          if (inputParameters.exportServerCerts == null)
          {
             answer = readOption("Save servers' certificates in an external key store (yes/no): ",
                                 YES_NO);
             inputParameters.exportServerCerts = BooleanOption.valueOf(answer.toUpperCase());
          }

          if (inputParameters.exportServerCerts == BooleanOption.YES)
          {
             saveServerCertificates();
          }

          // save the root CA in an external key store
          if (inputParameters.exportCAPrivateKey == null)
          {
             answer = readOption("Save the root CA private key in an external key store (yes/no): ",
                                 YES_NO);
             inputParameters.exportCAPrivateKey = BooleanOption.valueOf(answer.toUpperCase());
          }

          if (inputParameters.exportCAPrivateKey == BooleanOption.YES)
          {
             saveRootCAPrivateKey();
          }

          System.out.println("Done.");
          System.out.println("WARNING! Any configuration set at the client.xml or server.xml files " +
                             "or via bootstrap config arguments will have priority over the " +
                             "in-directory keys or certificates.");
          System.out.println("WARNING! The private key encryption passwords for all the account " +
                             "certificates are saved unencrypted in the directory.  The root CA's " +
                             "private key can be safely deleted, if it is not required to issue " +
                             "other certificates using this CA.");
          if (masterPassword != null)
          {
             System.out.println("WARNING! All private keys are encrypted using the same [" +
                                masterPassword + "] password. If needed, delete the 'key-password' " +
                                "nodes from the directory manually, and set this password using the " +
                                "access:password:masterkeyentry bootstrap config at server startup.");
          }
       }

       /**
        * Includes the target certificate suite into the new key store and saves the key store content
        * into the file named by this template [alias]-web-key.store.
        *
        * @param    suite
        *           The certificate suite
        * @param    alias
        *           The web certificate alias
        *
        * @throws   SSLCertGenException
        *           If the target certificate suite is not saved.
        */
       private void saveToWebStore(CertificateSuite suite, String alias)
       throws SSLCertGenException
       {
          String pkFile = String.format("%s-web-key.store", alias);

          KeyStore pkStore;
          try
          {
             pkStore = createEmptyStore();

             if (!reusePasswords || (webPrivateKeyPassword == null))
             {
                webPrivateKeyPassword = createAES256BitKey();
             }

             String password = webPrivateKeyPassword;

             pkStore.setKeyEntry(alias, suite.privateKey, password.toCharArray(), suite.getFullChain());

             String ksPass = webPrivateKeyPassword;

             pkStore.store(new FileOutputStream(pkFile), ksPass.toCharArray());

             String msg = "The web certificate suite [%s] was saved in the [%s] " +
                          "key store, using store password [%s] and key-entry password [%s].";
             System.out.println(String.format(msg, alias, pkFile, ksPass, password));
          }
          catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException ex)
          {
             throw new SSLCertGenException("saveToWebStore is failed", ex);
          }
       }

       /**
        * Updates the web certificates and saves them into the external key store.
        *
        * @param    serverId
        *           The server identifier
        *
        * @throws   SSLCertGenException
        *           If it is failed.
        */
       private void updateWebCertificates(String serverId)
       throws SSLCertGenException
       {
          CertificateSuite suite;
          // if there is no web certificate alias for the given server
          if (webAlias == null)
          {
             // use alias given by the parameter
             webAlias = inputParameters.webAlias;
          }

          if (webAlias != null)
          {
             if (this.aliases.contains(webAlias))
             {
                // web certificates are reused from the server certificates
                suite = getCertificateSuite(webAlias);
                webPrivateKeyPassword = createAES256BitKey(webAlias);
                this.webCerts.setCertificateSuite(suite, ds, serverId, webAlias, webPrivateKeyPassword);
             }
             else
             {
                suite = this.webCerts.getWebCertificates(ds, serverId);
             }

             saveToWebStore(suite, webAlias);
          }
       }

       /**
        * Gets the certificate suite for the given server alias from the directory.
        *
        * @param    serverAlias
        *           The given server alias
        *
        * @return   The certificate suite
        *
        * @throws   SSLCertGenException
        *           If it can't get certificate suite for the provided alias
        */
       private CertificateSuite getCertificateSuite(String serverAlias)
       throws SSLCertGenException
       {
          try
          {
             CertificateFactory cf = CertificateFactory.getInstance("X.509");

             String fullId = ROOT_PEER_NODE + serverAlias;

             // read the certificate data from directory
             byte[] data = ds.getNodeByteArray(fullId, "value");

             ByteArrayInputStream bis  = new ByteArrayInputStream(data);

             X509Certificate cert = (X509Certificate)cf.generateCertificate(bis);

             List<X509Certificate> chainList = new LinkedList<X509Certificate>();
             // read chain from cas
             String[] chainAliases = ds.enumerateNodes(ROOT_CA_NODE);

             for(String alias : chainAliases)
             {
                fullId = ROOT_CA_NODE + alias;
                data = ds.getNodeByteArray(fullId, "value");
                bis  = new ByteArrayInputStream(data);
                chainList.add((X509Certificate)cf.generateCertificate(bis));
             }
             String containerId  = ROOT_PRIVATE_KEYS_NODE +  serverAlias;
             String kpId = containerId  + "/key-password";
             String keId = containerId  + "/key-entry";

             byte[] kpData = ds.getNodeByteArray(kpId, "value");
             String keyEntryPassword = new String(kpData);

             byte[] encrypted = ds.getNodeByteArray(keId, "value");

             PrivateKey pkey = factory.decryptPrivateKey(encrypted, keyEntryPassword);

             CertificateSuite suite = new CertificateSuite();

             suite.privateKey = pkey;
             suite.publicKey  = cert;
             suite.chain      = chainList.toArray(new X509Certificate[chainList.size()]);
             return suite;
          }
          catch(Throwable ex)
          {
             throw new SSLCertGenException("Can't get certificate suite for " + serverAlias, ex);
          }
       }

       /**
        * Returns a full chain for the given generated certificate signed by the root CA certificate
        *
        * @param    leaf
        *           The given generated certificate
        *
        * @return   The full certificate chain.
        */
       private Certificate[] getFullChain(Certificate leaf)
       {
          Certificate[] rootFullChain = factory.getFullChainFromRoot();

          Certificate[] fullChain = new Certificate[rootFullChain.length + 1];

          fullChain[0] = leaf;

          System.arraycopy(rootFullChain, 0, fullChain, 1, rootFullChain.length);

          return fullChain;
       }

       /**
        * Save public and private certificates under the given alias in the generated external
        * key store given by this file name template "%s-private-key.store".
        *
        * @param    alias
        *           The certificate alias
        *
        * @throws   SSLCertGenException
        *           If certificates weren't saved in the external file due to the underlined exception
        *           thrown by IO API or Cryptography API.
        */
       private void saveCertificatePairInExternalStore(String alias)
       throws SSLCertGenException
       {
          KeyStore pkStore = null;
          String pkFile = null;

          boolean server = serverAliases.contains(alias);
          KeyStore certStore = (server ? trustCertStore : accCertStore);
          KeyStore keyStore  = (server ? trustKeyStore  : accKeyStore);

          pkFile = String.format("%s-private-key.store", alias);

          pkStore = createEmptyStore();

          try
          {
             Key         key  = keyStore.getKey(alias, keyEntryPasswords.get(alias).toCharArray());

             Certificate cert = certStore.getCertificate(alias);

             String password = (reusePasswords && directoryPasswords.containsKey(alias)
                                  ? directoryPasswords.get(alias)
                                  : keyEntryPasswords.get(alias));

             pkStore.setKeyEntry(alias, key, password.toCharArray(), getFullChain(cert));

             String ksPass = readPassword(alias);
             pkStore.store(new FileOutputStream(pkFile), ksPass.toCharArray());

             String msg = "The private key for certificate [%s] was saved in the [%s] " +
                          "key store, using store password [%s] and key-entry password [%s].";
             System.out.println(String.format(msg, alias, pkFile, ksPass, password));
          }
          catch (UnrecoverableKeyException |
                 KeyStoreException         |
                 NoSuchAlgorithmException  |
                 IOException               |
                 CertificateException e)
          {
             throw new SSLCertGenException(e);
          }
       }

       /**
        * Save the root CA private key in an external key store.
        *
        * @throws    IOException
        *            If standard input can not be accessed.
        * @throws    SSLCertGenException
        *            In case of problems during key store access.
        */
       private void saveRootCAPrivateKey()
       throws IOException,
              SSLCertGenException
       {
          String file = null;
          if (inputParameters.rootPrivateKeysStore == null)
          {
             do
             {
                file = readLine("Enter key store file name: ");
             }
             while (file.length() == 0);
          }
          else
          {
             file = inputParameters.rootPrivateKeysStore;
          }

          try
          {
             String kepass = keyEntryPasswords.get(rootCAAlias);

             Key rootKey = this.trustKeyStore.getKey(rootCAAlias, kepass.toCharArray());

             KeyStore store = createEmptyStore();
             store.setKeyEntry(rootCAAlias,
                               rootKey,
                               kepass.toCharArray(),
                               factory.getFullChainFromRoot());

             String kspass = RandomWordGenerator.create();
             store.store(new FileOutputStream(file), kspass.toCharArray());

             String msg =
                "The root CA's private key and certificate were saved in the [%s] store.\n" +
                "The key store was encrypted using the [%s] password, while the root " +
                "CA's private key was encrypted using the [%s] password.";
             System.out.println(String.format(msg, file, kspass, kepass));
          }
          catch (KeyStoreException         |
                 NoSuchAlgorithmException  |
                 UnrecoverableKeyException |
                 CertificateException e)
          {
             throw new SSLCertGenException(e);
          }
       }

       /**
        * Save the server certificates in an external store.
        *
        * @throws    IOException
        *            If standard input can not be accessed.
        * @throws    SSLCertGenException
        *            In case of problems during key store access.
        */
       private void saveServerCertificates()
       throws IOException,
              SSLCertGenException
       {
          // save root CA certificate
          if (inputParameters.includeCACert == null)
          {
             String answer = readOption("Include the root CA certificate, to use this store as a " +
                                        "trust store (yes/no)? ", YES_NO);
             inputParameters.includeCACert = BooleanOption.valueOf(answer.toUpperCase());
          }

          boolean saveRootCA = inputParameters.includeCACert == BooleanOption.YES;

          String file = null;
          if (inputParameters.serverCertsStore == null)
          {
             do
             {
                file = readLine("Enter key store file name: ");
             }
             while (file.length() == 0);
          }
          else
          {
             file = inputParameters.serverCertsStore;
          }
          try
          {
             KeyStore store = createEmptyStore();
             String firstAlias = null;
             for (String alias : serverAliases)
             {
                Certificate cert = trustCertStore.getCertificate(alias);

                store.setCertificateEntry(alias, cert);

                if (firstAlias == null)
                {
                   firstAlias = alias;
                }
             }

             if (saveRootCA)
             {
                store.setCertificateEntry(rootCAAlias, trustCertStore.getCertificate(rootCAAlias));

                Certificate[] fullChain = factory.getFullChainFromRoot();

                StringBuilder interAlias = new StringBuilder(rootCAAlias);

                for(int i = 1; i < fullChain.length; i++)
                {
                   interAlias.append("-").append(i);
                   store.setCertificateEntry(interAlias.toString(), fullChain[i]);
                }
             }

             String pass = readPassword(firstAlias);
             store.store(new FileOutputStream(file), pass.toCharArray());

             String msg = "The servers' certificates %s were saved in the [%s] key store, " +
                          "using password [%s].";
             System.out.println(String.format(msg, serverAliases.toString(), file, pass));
          }
          catch (KeyStoreException | NoSuchAlgorithmException | CertificateException e)
          {
             throw new SSLCertGenException(e);
          }
       }

       /**
        * Get the reused password or generate a new one, for the given alias.
        *
        * @param    alias
        *           The certificate alias.
        *
        * @return   See above.
        */
       private String readPassword(String alias)
       {
          if (reusePasswords)
          {
             if (directoryPasswords.containsKey(alias))
             {
                return directoryPasswords.get(alias);
             }
          }

          return RandomWordGenerator.create();
       }

       /**
        * Save the private keys in external key store(s).
        *
        * @param    server
        *           <code>true</code> to save the server private keys; <code>false</code> to save
        *           the account private-keys.
        *
        * @throws   IOException
        *           If standard input can not be accessed.
        * @throws   SSLCertGenException
        *           In case of problems during key store access.
        */
       private void savePrivateKeys(boolean server)
       throws IOException,
              SSLCertGenException
       {
          // save the private keys in external key store

          if ((server  && inputParameters.useCommonServerKeyStore  == null) ||
              (!server && inputParameters.useCommonAccountKeyStore == null))
          {

             String answer = readOption("Save the private keys in the same key store (yes/no): ",
                                        YES_NO);
             if (server)
             {
                inputParameters.useCommonServerKeyStore = BooleanOption.valueOf(answer.toUpperCase());
             }
             else
             {
                inputParameters.useCommonAccountKeyStore = BooleanOption.valueOf(answer.toUpperCase());
             }
          }

          boolean samePKStore;
          if (server)
          {
             samePKStore = inputParameters.useCommonServerKeyStore == BooleanOption.YES;
          }
          else
          {
             samePKStore = inputParameters.useCommonAccountKeyStore == BooleanOption.YES;
          }

          KeyStore pkStore = null;
          String pkFile = null;

          KeyStore certStore = (server ? trustCertStore : accCertStore);
          KeyStore keyStore  = (server ? trustKeyStore  : accKeyStore);

          for (String alias : aliases)
          {
             if (server != serverAliases.contains(alias))
             {
                // skip, if the alias is not for a server (and server is used) or if alias is for a
                // server (and account is used)
                continue;
             }

             if ((samePKStore && pkStore == null) || !samePKStore)
             {
                if (samePKStore)
                {
                   if ((server  && inputParameters.commonServerKeyStore  == null) ||
                       (!server && inputParameters.commonAccountKeyStore == null))
                   {
                      do
                      {
                         pkFile = readLine("Enter key store file name: ");
                      }
                      while (pkFile.length() == 0);
                   }
                   else
                   {
                      if (server)
                      {
                         pkFile = inputParameters.commonServerKeyStore;
                      }
                      else
                      {
                         pkFile = inputParameters.commonAccountKeyStore;
                      }
                   }
                }
                else
                {
                   pkFile = String.format("%s-private-key.store", alias);
                }

                pkStore = createEmptyStore();
             }

             try
             {
                Key         key  = keyStore.getKey(alias, keyEntryPasswords.get(alias).toCharArray());
                Certificate cert = certStore.getCertificate(alias);

                String password = (reusePasswords && directoryPasswords.containsKey(alias)
                                     ? directoryPasswords.get(alias)
                                     : keyEntryPasswords.get(alias));
                pkStore.setKeyEntry(alias, key, password.toCharArray(), getFullChain(cert));

                if (!samePKStore)
                {
                   String ksPass = readPassword(alias);
                   pkStore.store(new FileOutputStream(pkFile), ksPass.toCharArray());

                   String msg = "The private key for certificate [%s] was saved in the [%s] " +
                                "key store, using store password [%s] and key-entry password [%s].";
                   System.out.println(String.format(msg, alias, pkFile, ksPass, password));
                }
                else
                {
                   String msg = "Adding the private key for certificate [%s] to store [%s], " +
                                "using key-entry password [%s]...";
                   System.out.println(String.format(msg, alias, pkFile, password));
                }
             }
             catch (UnrecoverableKeyException |
                    KeyStoreException         |
                    NoSuchAlgorithmException  |
                    CertificateException e)
             {
                throw new SSLCertGenException(e);
             }
          }

          if (samePKStore)
          {
             try
             {
                String ksPass = RandomWordGenerator.create();
                pkStore.store(new FileOutputStream(pkFile), ksPass.toCharArray());

                String msg = "The key store [%s] was encrypted using password [%s].";
                System.out.println(String.format(msg, pkFile, ksPass));
             }
             catch (KeyStoreException | NoSuchAlgorithmException | CertificateException e)
             {
                throw new SSLCertGenException(e);
             }
          }
       }

       /**
        * Save all the root CA certificate and its private key in the directory.
        *
        * @throws    SSLCertGenException
        *            If the data could not be saved.
        */
       private void saveRootCA()
       throws SSLCertGenException
       {
          NodeHelper.openBatch(ds, ROOT_CERTIFICATES_NODE);

          savePrivateKey(rootCAAlias, trustKeyStore);

          saveCertificate(rootCAAlias, trustCertStore, ROOT_CA_NODE);

          StringBuilder interAlias = new StringBuilder(rootCAAlias);

          Certificate[] fullChain = factory.getFullChainFromRoot();

          for (int i = 1; i < fullChain.length; i++)
          {
             interAlias.append("-").append(i);

             String fullId = ROOT_CA_NODE + interAlias.toString();

             NodeHelper.deleteNode(ds, ROOT_CA_NODE + interAlias.toString());

             System.out.println(String.format("Adding intermediate certificate for alias [%s] to the %s node...",
                                              rootCAAlias,
                                              fullId));
             try
             {
                Certificate cert = fullChain[i];
                NodeHelper.addNode(ds, fullId, "bytes", cert.getEncoded());
             }
             catch (CertificateEncodingException e)
             {
                throw new SSLCertGenException(e);
             }
          }

          NodeHelper.closeBatch(ds, ROOT_CERTIFICATES_NODE);
       }

       /**
        * Save all the peer certificates and their private keys in the directory.
        *
        * @throws    SSLCertGenException
        *            If the data could not be saved.
        */
       private void saveCertificates()
       throws SSLCertGenException
       {
          for (String alias : aliases)
          {
             saveCertificatePair(alias);
          }
       }

       /**
        * Save the public and private certificates for the given alias from the directory.
        *
        * @param    alias
        *           The given alias
        *
        * @throws   SSLCertGenException
        *           If the data could not be saved.
        */
       private void saveCertificatePair(String alias)
       throws SSLCertGenException
       {
          NodeHelper.openBatch(ds, ROOT_CERTIFICATES_NODE);

          boolean isServer = serverAliases.contains(alias);

          savePrivateKey(alias, (isServer ? trustKeyStore : accKeyStore));

          saveCertificate(alias, (isServer ? trustCertStore : accCertStore), ROOT_PEER_NODE);

          NodeHelper.closeBatch(ds, ROOT_CERTIFICATES_NODE);
       }

       /**
        * Save the certificate for the specified alias in the directory, under the specified parent
        * node.
        *
        * @param    alias
        *           The alias.
        * @param    certStore
        *           The store from which to retrieve the certificate.
        * @param    parentNode
        *           The parent node where to save the certificate.
        *
        * @throws   SSLCertGenException
        *           If the data could not be saved.
        */
       private void saveCertificate(String alias, KeyStore certStore, String parentNode)
       throws SSLCertGenException
       {
          String fullId = parentNode + alias;
          NodeHelper.deleteNode(ds, fullId);

          System.out.println(String.format("Adding certificate for alias [%s] to the %s node...",
                                           alias, fullId));

          try
          {
             Certificate cert = certStore.getCertificate(alias);
             MessageDigest complete = MessageDigest.getInstance("MD5");
             complete.update(cert.getEncoded());
             byte[] checksum = complete.digest();
             String key = "";
             for (int i = 0; i < checksum.length; i++)
             {
                key += Integer.toString((checksum[i] & 0xff) + 0x100, 16).substring(1);
             }
             System.out.println(String.format("Certificate for alias [%s] has checksum [%s]", alias, key));

             NodeHelper.addNode(ds, fullId, "bytes", cert.getEncoded());
          }
          catch (KeyStoreException | CertificateEncodingException | NoSuchAlgorithmException e)
          {
             throw new SSLCertGenException(e);
          }

       }

       /**
        * Save the private key for the specified alias in the directory.
        * <p>
        * The private key is encrypted using a random password, which will be saved too in the
        * directory.
        *
        * @param    alias
        *           The alias.
        * @param    privateKeyStore
        *           The store from which to read the private key.
        *
        * @throws   SSLCertGenException
        *           If the private key could not be saved.
        */
       private void savePrivateKey(String alias, KeyStore privateKeyStore)
       throws SSLCertGenException
       {
          String fullId = ROOT_PRIVATE_KEYS_NODE + alias;

          if (ds.getNodeClass(ROOT_PRIVATE_KEYS_NODE) == null)
          {
             NodeHelper.addNode(ds, ROOT_PRIVATE_KEYS_NODE, "container", null);
          }

          String password = keyEntryPasswords.get(alias);
          boolean rootCA = rootCAAlias.equals(alias);

          // determine which password to use: master or distinct
          String dirPassword = (masterPassword != null && !rootCA)
                                  ? masterPassword
                                  : rootCA && reuseRootCA ? rootCAPassword : createAES256BitKey(alias);

          NodeHelper.deleteNode(ds, fullId);
          NodeHelper.addNode(ds, fullId, "container", null);

          if (!rootCA)
          {
             // do not save the root CA password
             NodeHelper.addNode(ds, ROOT_PRIVATE_KEYS_NODE + alias + "/key-password", "bytes", dirPassword.getBytes());
          }

          directoryPasswords.put(alias, dirPassword);

          try
          {
             Key key = privateKeyStore.getKey(alias, password.toCharArray());

             byte[] encrypted = factory.encryptPrivateKey(key, dirPassword);
             NodeHelper.addNode(ds, ROOT_PRIVATE_KEYS_NODE + alias + "/key-entry", "bytes", encrypted);
          }
          catch (UnrecoverableKeyException | KeyStoreException | NoSuchAlgorithmException e)
          {
             throw new SSLCertGenException(e);
          }

          final String msg1 = "Private key for alias [%s] was encrypted using the [%s] password " +
                              "and saved in the %s node.\nUse the access:password:keyentry-%s key " +
                              "to set this password in the server's bootstrap config file and " +
                              "delete  the /security/certificates/private-keys/%s node from " +
                              "the directory.";
          final String msg2 = "Private key for root CA [%s] was encrypted using the [%s] password " +
                              "and the password was NOT SAVED in the directory.  Losing this " +
                              "password will prohibit using the root CA to issue new certificates.";
          if (rootCA)
          {
             System.out.println(String.format(msg2, alias, dirPassword));
          }
          else
          {
             System.out.println(String.format(msg1, alias, dirPassword, fullId, alias, alias));
          }
       }

       /**
        * Generate peer certificates for all {@link #aliases}.
        *
        * @throws    SSLCertGenException
        *            If the SSL certificates could not be generated.
        */
       private void generateCertificates()
       throws SSLCertGenException
       {
          for (String alias : aliases)
          {
             generateCertificate(alias);
          }
       }

       /**
        * Generate peer certificates for the given alias.
        *
        * @param    alias
        *           The given alias
        *
        * @throws   SSLCertGenException
        *           If the SSL certificates could not be generated.
        */
       private void generateCertificate(String alias)
       throws SSLCertGenException
       {
          boolean isServer = (serverAliases.contains(alias));
          String type = (isServer ? "server" : "account");
          String commonName = alias + " " + type;

          System.out.println(String.format("Generating certificate for %s with alias [%s]...",
                                           type, alias));

          String customCN = ds.getNodeString(ROOT_CERT_COMMON_NAME + "/" + alias, "value");
          if (customCN != null)
          {
             commonName = customCN;
          }

          String password = factory.generateCertificate(alias, validity,
                                                        commonName, company,
                                                        (isServer ? trustCertStore : accCertStore),
                                                        (isServer ? trustKeyStore : accKeyStore));

          keyEntryPasswords.put(alias, password);
       }

       /**
        * Generate the root CA if there is no CA certificate in the directory or the reuse root CA
        * option is not set, otherwise read the existing root CA from the directory.
        *
        * @throws    SSLCertGenException
        *            If the root CA could not be generated.
        * @throws    IOException
        *            If the alias for the root CA could not be read from standard input.
        */
       private void generateRootCA()
       throws SSLCertGenException,
              IOException
       {
          // find the root CA alias
          NodeHelper.openBatch(ds, ROOT_CA_NODE);

          String[] cas = ds.enumerateNodes(ROOT_CA_NODE);
          if (cas.length == 0)
          {
             boolean hasAlias = inputParameters.alias != null;
             // read the root CA alias
             while (rootCAAlias == null || rootCAAlias.length() == 0)
             {
                if (hasAlias)
                {
                   rootCAAlias = inputParameters.alias;
                }
                else
                {
                   rootCAAlias = readLine("Enter the root CA alias: ");
                }

                if (!aliases.contains(rootCAAlias))
                {
                   break;
                }
                String errorWarning = "Alias " + rootCAAlias + " is in use!";
                if (hasAlias)
                {
                   throw new SSLCertGenException(errorWarning);
                }
                System.out.println(errorWarning);
                rootCAAlias = null;
             }

             reuseRootCA = false;
             rootCAPassword = null;
          }
          else
          {
             // reuse the root CA alias
             rootCAAlias = cas[0];
          }

          NodeHelper.closeBatch(ds, ROOT_CA_NODE);

          String msg = reuseRootCA ? "Re-using root CA using [%s] alias..."
                                   : (externalRootCA ? "Loading the externally generated root CA" :
                                      "Generating root CA using [%s] alias...");
          System.out.println(String.format(msg, rootCAAlias));

          String password;
          if (reuseRootCA)
          {
             try
             {
                password = rootCAPassword;

                // read the root CA from the directory (certificate and private key)
                byte[] bpk = ds.getNodeByteArray(ROOT_PRIVATE_KEYS_NODE + rootCAAlias + "/key-entry",
                                                 "value");
                byte[] bcert = ds.getNodeByteArray(ROOT_CERTIFICATES_NODE + "/cas/" + rootCAAlias,
                                                   "value");

                PrivateKey pk = factory.decryptPrivateKey(bpk, rootCAPassword);
                CertificateFactory cf = CertificateFactory.getInstance("X.509");
                X509Certificate cert = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(bcert));

                String[] aliases = ds.enumerateNodes(ROOT_CA_NODE);
                // read chain
                StringBuilder interAlias = new StringBuilder(rootCAAlias);

                List<Certificate> chainList = new LinkedList<>();

                for (int i = 1; i < aliases.length; i++)
                {
                   interAlias.append("-").append(i);

                   String fullId = ROOT_CA_NODE + interAlias.toString();
                   byte[] bytes = ds.getNodeByteArray(fullId, "value");
                   if (bytes != null)
                   {
                      X509Certificate interCert = (X509Certificate) cf.generateCertificate(
                               new ByteArrayInputStream(bytes));
                      chainList.add(interCert);
                   }
                }

                if (!chainList.isEmpty())
                {
                   Certificate[] chain = chainList.toArray(new Certificate[0]);
                   // store the given chain
                   factory.setCertificateChain(chain);
                }
                // store the given root CA
                factory.setRootCA(cert, pk);

                trustCertStore.setCertificateEntry(rootCAAlias, cert);

                // get stored full chain from the factory
                trustKeyStore.setKeyEntry(rootCAAlias,
                                          pk,
                                          rootCAPassword.toCharArray(),
                                          factory.getFullChainFromRoot());
             }
             catch (Exception e)
             {
                throw new SSLCertGenException(e);
             }
          }
          else
          {
             if (externalRootCA)
             {
                //TODO try catch block for the input verification
                String certPemFile;
                if (inputParameters.externalRootCA.containsKey(ExternalCertificates.cert))
                {
                   certPemFile = inputParameters.externalRootCA.get(ExternalCertificates.cert);
                }
                else
                {
                   certPemFile = readLine("Enter a root CA certificate pem file path:");
                }
                String privateKeyPemFile;
                if (inputParameters.externalRootCA.containsKey(ExternalCertificates.key))
                {
                   privateKeyPemFile = inputParameters.externalRootCA.get(ExternalCertificates.key);
                }
                else
                {
                   privateKeyPemFile = readLine("Enter an associated private key pem file path:");
                }
                String certChainPemFile;
                if (inputParameters.externalRootCA.containsKey(ExternalCertificates.chain))
                {
                   certChainPemFile = inputParameters.externalRootCA.get(ExternalCertificates.chain);
                }
                else
                {
                   // if it is empty, then certChainPemFile is ignored
                   certChainPemFile = readLine("Enter an associated intermediate certificates"
                            + " chain pem file path:");
                }
                // if it is empty, then certChainPemFile is ignored
                CertificateSuite suite = factory.loadCertificateSuite(certPemFile,
                                                                      privateKeyPemFile,
                                                                      certChainPemFile);
                factory.setRootCA(suite.publicKey, suite.privateKey);
                factory.setCertificateChain(suite.chain);
                password = factory.saveRootCA(rootCAAlias, trustCertStore, trustKeyStore);
             }
             else
             {
                password = factory.generateRootCA(rootCAAlias, validity,
                         rootCAAlias + " - root CA authority", company,
                         trustCertStore, trustKeyStore);
             }
          }

          keyEntryPasswords.put(rootCAAlias, password);
       }

       /**
        * Read all the accounts which have an alias specified.
        *
        * @param    rootNode
        *           The node from which to read the accounts.
        */
       private void readAccounts(String rootNode)
       {
          NodeHelper.openBatch(ds, rootNode);

          String[] accIds = ds.enumerateNodes(rootNode);
          if (accIds.length == 0)
          {
             System.out.println("No accounts defined in " + rootNode + "!");
             return;
          }

          String  alias    = null;
          Boolean isServer = null;

          // process existing nodes
          for (int i = 0; i < accIds.length; i++)
          {
             String accID = accIds[i];
             String fullId = new String(rootNode + accID);
             String nodeCls = ds.getNodeClass(fullId);

             if ("/meta/class/process".equals(nodeCls))
             {
                isServer = ds.getNodeBoolean(fullId, "server");
             }
             else if ("/meta/class/user".equals(nodeCls))
             {
                // user account
             }
             else
             {
                System.out.println("Account " + fullId + " ignored - not a user or process");
                continue;
             }

             alias    = ds.getNodeString(fullId,  "alias");

             if (alias == null || alias.length() == 0)
             {
                System.out.println("Account " + fullId + " ignored - no alias is defined");
                continue;
             }

             if (aliases.contains(alias))
             {
                System.out.println("WARNING: alias " + alias + " is set for multiple accounts!");
                continue;
             }

             System.out.println("Adding account " + fullId);

             aliases.add(alias);

             if (isServer != null && isServer.booleanValue())
             {
                serverAliases.add(alias);
             }
          }

          NodeHelper.closeBatch(ds, rootNode);
       }

       /**
        * Read the existing company configuration from the directory or, if missing, read it from
        * the standard input.
        *
        * @throws    IOException
        *            In case of problems during reading.
        * @throws    SSLCertGenException
        *            If the {@link SSLCertFactory} factory could not be initialized.
        */
       private void readCompanyConfiguration()
       throws IOException,
              SSLCertGenException
       {
          Map<String, String> mandatoryKeys = factory.getMandatorySubjectFields();

          while (!ds.openBatch(ROOT_COMPANY_NODE))
          {
             // node does not exist, add it
             NodeHelper.openBatch(ds, ROOT_CERTIFICATES_NODE);

             // add a new node
             NodeHelper.addNode(ds, ROOT_COMPANY_NODE, "container", null);

             NodeHelper.closeBatch(ds, ROOT_CERTIFICATES_NODE);
          }

          Integer rsaKeyStrength = null;
          BigInteger rsaPublicExponent = null;

          // read the existing nodes first
          String[] existing = ds.enumerateNodes(ROOT_COMPANY_NODE);
          for (int i = 0; i < existing.length; i++)
          {
             String oid = existing[i].toUpperCase();

             if (oid.equalsIgnoreCase(CERT_VALIDITY_YEARS_NODE))
             {
                validity = ds.getNodeInteger(ROOT_COMPANY_NODE + oid, "value");
                System.out.println(String.format("Using %d years for validity.", validity));
                continue;
             }

             if (oid.equalsIgnoreCase(RSA_PRIVATE_KEY_STRENGTH))
             {
                rsaKeyStrength = ds.getNodeInteger(ROOT_COMPANY_NODE + oid, "value");
                System.out.println(String.format("Using %d bits for RSA private key strength.",
                                                 rsaKeyStrength));
                continue;
             }

             if (oid.equalsIgnoreCase(RSA_PUBLIC_EXPONENT))
             {
                // the exponent can be a very large or even a hex value, so keep it in a string.
                String exp = ds.getNodeString(ROOT_COMPANY_NODE + oid, "value");
                rsaPublicExponent = new BigInteger(exp);
                System.out.println(String.format("Using %s for RSA public exponent.", exp));
                continue;
             }

             if (oid.equalsIgnoreCase("hostnames"))
             {
                continue;
             }

             String val = ds.getNodeString(ROOT_COMPANY_NODE + oid, "value").trim();

             // existing value (may be empty)
             String txt = mandatoryKeys.get(oid);
             System.out.println(String.format("Using '%s' for [%s] %s.", val, oid,
                                              (txt == null ? "" : txt)));

             mandatoryKeys.remove(oid);
             company.put(oid, val);
          }

          // get user input for the remainder of the nodes
          for (String oid : mandatoryKeys.keySet())
          {
             String txt = mandatoryKeys.get(oid);

             // read the value
             String val = inputParameters.subjectFields == null
                            ? null
                            : inputParameters.subjectFields.get(RequestInfo.valueOf(oid));

             if (val == null || val.trim().isEmpty())
             {
                val = readLine(String.format("%s [%s]: ", txt, oid));
             }

             NodeHelper.addNode(ds, ROOT_COMPANY_NODE + oid, "string", val);

             company.put(oid, val);

             // save empty values
             System.out.println(String.format("Using '%s' for [%s] %s.", val, oid, txt));
          }

          // check the certificate validity
          if (validity == -1)
          {
             while (true)
             {
                try
                {
                   validity = Integer.parseInt(readLine("Enter validity (years): "));

                   if (validity <= 0)
                   {
                      System.out.println("Enter a number of years greater than zero.");
                      continue;
                   }

                   break;
                }
                catch (NumberFormatException e)
                {
                   System.out.println("Incorrect year.");
                   continue;
                }
             }

             NodeHelper.addNode(ds, ROOT_COMPANY_NODE + CERT_VALIDITY_YEARS_NODE, "integer", validity);

             System.out.println(String.format("Using %d years for validity.", validity));
          }

          // get the key strength
          if (inputParameters.rsaKeySize != null)
          {
             if (inputParameters.rsaKeySize < SSLCertFactory.MIN_RSA_KEY_STRENGTH)
             {
                throw new SSLCertGenException(
                   String.format(
                      "The key size must be greater or equal than %d bits for RSA private key strength.",
                      SSLCertFactory.MIN_RSA_KEY_STRENGTH));
             }
             //override directory settings
             rsaKeyStrength = inputParameters.rsaKeySize;
          }

          if (rsaKeyStrength == null)
          {
             do
             {
                String msg = "Enter RSA key strength (in bits), greater or equal than " +
                             "%d (blank for default %d): ";
                msg = String.format(msg,
                                    SSLCertFactory.MIN_RSA_KEY_STRENGTH,
                                    SSLCertFactory.MIN_RSA_KEY_STRENGTH);
                String line = readLine(msg);
                rsaKeyStrength = (line.length() > 0) ? Integer.valueOf(line)
                                                     : SSLCertFactory.MIN_RSA_KEY_STRENGTH;

                if (rsaKeyStrength > SSLCertFactory.MIN_RSA_KEY_STRENGTH)
                {
                   NodeHelper.addNode(ds, ROOT_COMPANY_NODE + RSA_PRIVATE_KEY_STRENGTH, "integer", rsaKeyStrength);

                   System.out.println(String.format("Using %d bits for RSA private key strength.",
                                                    rsaKeyStrength));
                }
             }
             while (rsaKeyStrength < SSLCertFactory.MIN_RSA_KEY_STRENGTH);
          }

          // get the public exponent
          if (inputParameters.rsaPublicExponent != null)
          {
             //override directory settings
             rsaPublicExponent = inputParameters.rsaPublicExponent;
          }

          if (rsaPublicExponent == null)
          {
             String line = readLine("Enter RSA public exponent, blank for default (65537): ");
             if (line.length() > 0)
             {
                rsaPublicExponent = new BigInteger(line);

                NodeHelper.addNode(ds, ROOT_COMPANY_NODE + RSA_PUBLIC_EXPONENT,
                        "string", rsaPublicExponent.toString());

                System.out.println(String.format("Using %s for RSA public exponent.",
                                                 rsaPublicExponent.toString()));
             }
             else
             {
                rsaPublicExponent = RSA_PUBLIC_EXPONENT_DEFAULT_VALUE;
             }
          }

          factory.init(rsaKeyStrength, rsaPublicExponent);

          NodeHelper.closeBatch(ds, ROOT_COMPANY_NODE);
       }

       /**
        * Read a line of text using the created {@link #reader}.
        *
        * @param    txt
        *           Description to be written to standard output.
        *
        * @return   The line of text from standard input.
        *
        * @throws   IOException
        *           If data could not be read.
        */
       private String readLine(String txt)
       throws IOException
       {
          if (inputs != null)
          {
             return inputs[currentInputIdx++];
          }

          System.out.print(txt);

          String line = reader.readLine();

          return line.trim();
       }

       /**
        * Ask the user to enter one of the specified valid options, using the given message.
        *
        * @param    msg
        *           The message shown to the user.
        * @param    valid
        *           An array of valid options.
        */
       private String readOption(String msg, String[] valid)
       throws IOException
       {
          String answer = null;
          l1: do
          {
             answer = readLine(msg);

             for (String s : valid)
             {
                if (answer.equalsIgnoreCase(s))
                {
                   break l1;
                }
             }
          }
          while (true);

          return answer;
       }

       /**
        * Create an empty store, to hold either private keys or certificates.
        *
        * @return   An empty {@link KeyStore} instance.
        *
        * @throws   SSLCertGenException
        *           If the store could not be generated.
        */
       static KeyStore createEmptyStore()
       throws SSLCertGenException
       {
          try
          {
             KeyStore ks = KeyStore.getInstance("JKS");
             ks.load(null, null);
             return ks;
          }
          catch (NoSuchAlgorithmException | CertificateException | IOException | KeyStoreException e)
          {
             throw new SSLCertGenException(e);
          }
       }

       /**
        * Create a random 256-bit password to be used as an AES encryption key.
        * <p>
        * If {@link #reusePasswords} is on, and there is a password in the directory, that will be
        * returned.
        *
        * @param    alias
        *           The alias to check.
        *
        * @return   See above.
        */
       private String createAES256BitKey(String alias)
       {
          if (reusePasswords)
          {
             String node = ROOT_PRIVATE_KEYS_NODE + alias + "/key-password";
             byte[] bytes = ds.getNodeByteArray(node, "value");
             if (bytes != null)
             {
                return new String(bytes);
             }
          }

          return createAES256BitKey();
       }

       /**
        * Implements BigInteger option handler to get BigInterger from the decimal string.
        */
       public static class BigIntegerOptionHandler extends OneArgumentOptionHandler<BigInteger>
       {

          public BigIntegerOptionHandler(CmdLineParser parser, OptionDef option,
                   Setter<? super BigInteger> setter)
          {
             super(parser, option, setter);
          }

          @Override
          protected BigInteger parse(String argument) throws NumberFormatException, CmdLineException
          {
             return new BigInteger(argument);
          }
       }

       /**
        * Command line driver.
        *
        * @param    args
        *           Application command line parameters. File name is the only one expected.
        *
        * @throws    ConfigurationException
        *            In case the {@link #ds directory service} could not be initialized.
        * @throws    IOException
        *            If standard input can not be accessed.
        * @throws    SSLCertGenException
        *            In case of problems during the generation of root CA or peer certificates.
        */
       public static void main(String[] args)
       throws ConfigurationException,
              IOException,
              SSLCertGenException
       {
          OptionHandlerRegistry.getRegistry().registerHandler(BigInteger.class, BigIntegerOptionHandler.class);

          InputParameters inputParameters = new InputParameters();

          CmdLineParser parser = new CmdLineParser(inputParameters);
          try
          {
             parser.parseArgument(args);
          }
          catch (CmdLineException e)
          {
             LOG.severe("", e);

             StringOutputStream stringOutputStream = new StringOutputStream();
             parser.printUsage(stringOutputStream);
             LOG.info(stringOutputStream.getData());

             return;
          }

          BootstrapConfig cfg = new BootstrapConfig();

          cfg.setConfigItem("directory", "backend", "type",        "xml");
          cfg.setConfigItem("directory", "xml",     "filename",    inputParameters.directoryFileName);
          cfg.setConfigItem("directory", "xml",     "must_exist", "true");
          cfg.setServer(true);

          SSLCertGenUtil gen = new SSLCertGenUtil(cfg);
          gen.inputParameters = inputParameters;
          gen.generate();
       }

       /**
        * The enumeration type for boolean options
        */
       enum BooleanOption
       {
          YES,
          NO
       }

       /**
        * Defines option names to load external CA certificates.
        */
       enum ExternalCertificates
       {
          /** The external private certificate */
          key,

          /** The external public certificate */
          cert,

          /** The trusted certificate chain */
          chain
       }

       /**
        * Maps string key and value pairs into pairs with ExternalCertificates keys when these
        * parameters are parsed.
        */
       public static class LoadCertificatesMapper
       extends EnumMapper<ExternalCertificates>
       {

          /**
           * Creates this parameter mapper.
           *
           * @param    parser
           *           The command line parser
           * @param    option
           *           The parameter definition
           * @param    setter
           *           The parameter setter
           */
          public LoadCertificatesMapper(CmdLineParser parser, OptionDef option,
                   Setter<? super Map<?, ?>> setter)
          {
             super(parser, option, setter);

             creator = ExternalCertificates.class;
          }
       }

       /**
        * Maps string key and value pairs into pairs with RequestInfo keys when request info
        * parameters are parsed.
        */
       public static class RequestInfoMapper
       extends EnumMapper<RequestInfo>
       {

          /**
           * Creates this parameter mapper.
           *
           * @param    parser
           *           The command line parser
           * @param    option
           *           The parameter definition
           * @param    setter
           *           The parameter setter
           */
          public RequestInfoMapper(CmdLineParser parser, OptionDef option,
                   Setter<? super Map<?, ?>> setter)
          {
             super(parser, option, setter);

             creator = RequestInfo.class;
          }
       }

       /**
        * Maps string key and value pairs into pairs with keys given by the provided enumeration type.
        *
        * @param    E
        *           The enumeration type
        */
       public static abstract class EnumMapper<E extends Enum<E>>
       extends MapOptionHandler
       {

          /** The enumeration type */
          protected Class<E> creator;

          /**
           * Creates this parameter mapper.
           *
           * @param    parser
           *           The command line parser
           * @param    option
           *           The parameter definition
           * @param    setter
           *           The parameter setter
           */
          public EnumMapper(CmdLineParser parser, OptionDef option,
                   Setter<? super Map<?, ?>> setter)
          {
             super(parser, option, setter);
          }

          /**
           * Creates a typed map.
           *
           * @param    type
           *           The type of the parameter map
           *
           * @return   The map with keys given by the provided enumeration type.
           */
          @Override
          protected EnumMap<E, String> createNewCollection(Class<? extends Map> type)
          {
             return new EnumMap<E, String>(creator);
          }

          /**
           * Convert string key and value pairs into pairs with keys from the typed enumeration.
           *
           * @param    m
           *           The target map
           * @param    key
           *           The parameter key
           * @param    value
           *           The parameter value
           */
          @Override
          protected void addToMap(Map m, String key, String value)
          {
             E[] enumValues = creator.getEnumConstants();

             for(E el : enumValues)
             {
                if (el.name().equalsIgnoreCase(key))
                {
                   m.put(el, value);
                   break;
                }
             }
          }
       }

       /**
        * SSLCertGenUtil input parameters
        */
       static class InputParameters
       {
          /**
           * Sets mandatory subject fields for generated certificates:
           * -S OU="organization unit"
           * -S O="organization"
           * -S L="locality(city)"
           * -S ST="state(province name)"
           * -S C="Country Code"
           */
          @Option(name="-S", handler=RequestInfoMapper.class)
          public Map<RequestInfo, String> subjectFields;

          /**
           * Indicates if the current certificates protection passwords stored in the directory will be
           * reused or not. Can be set via "-P" option.
           */
          @Option(name="-P", aliases={"--reuse-passwords"}, usage="Reuse passwords from the directory")
          public BooleanOption reusePasswords;

          /**
           * Indicates if the in-directory private keys will be encrypted using the same password or
           * not. Can be set via "-M" option.
           */
          @Option(name="-M", usage="Encrypt the in-directory private keys using the same password")
          public BooleanOption useMasterPassword;

          /**
           * Indicates if the current root certificate stored in the directory will be reused or not.
           * Can be set via "-C" option.
           */
          @Option(name="-C",
                  aliases={"--reuse-ca"},
                  usage="Reuse root CA from the directory",
                  forbids={"-L"})
          public BooleanOption reuseRootCA;

          /**
           * Sets protection password for the root certificate's private key via
           * -PKPWD "bncjkhweuiwebnwefjkdchbmamasdkloc"
           */
          @Option(name="-PKPWD",
                  aliases={"--private-key-password"},
                  usage="Private key protection password")
          public String privateKeyPassword;

          /**
           * Indicates if the new root certificate will be loaded from the file system or not.
           * Can be set via "-L" option.
           * Sets new root certificate PEM file
           * -L cert=cert_pem_file.pem
           * Sets new private key associated with the new root certificate PEM file
           * -L key=private_key_pem_file.pem
           * Sets new root certificate chain PEM file
           * -L chain=chain_pem_file.pem
           */
          @Option(name="-L",
                  usage="Load external root certificate",
                  forbids={"-C"},
                  handler=LoadCertificatesMapper.class)
          public Map<ExternalCertificates, String> externalRootCA;

          /**
           * Sets the path to the directory xml file via -dir "./directory.xml"
           */
          @Option(name="-dir", required=true, usage="Directory file name")
          public String directoryFileName;

          /**
           * Sets the alias name to store new root certificate.
           */
          @Option(name="-alias", usage="The root certificate alias name")
          public String alias;

          /**
           * Indicates if accounts private keys will be saved in external key-stores. Can be set
           * via "-EAPK" option.
           */
          @Option(name="-EAPK",
                  aliases={"--export-account-keys"},
                  usage="Save the account private keys in external key-store(s)")
          public BooleanOption exportAccountPrivateKeys = BooleanOption.YES;

          /**
           * Indicates if server private keys will be saved in external key-stores. Can be set
           * via "-ESPK" option.
           */
          @Option(name="-ESPK",
                  aliases={"--export-server-keys"},
                  usage="Save the server private keys in external key-store(s)")
          public BooleanOption exportServerPrivateKeys = BooleanOption.YES;

          /**
           * Indicates if server certificates will be saved in external key-stores. Can be set
           * via "-ESC" option.
           */
          @Option(name="-ESC",
                  aliases={"--export-server-certs"},
                  usage="Save the server certificates in external key-store(s)")
          public BooleanOption exportServerCerts = BooleanOption.YES;

          /**
           * Indicates if the root CA certificate will be saved in the server certificates store as
           * a trust store. Can be set via "-ICA" option.
           */
          @Option(name="-ICA",
                  aliases={"--include-ca"},
                  usage="Include the root CA certificate in the server certificates store as a trust store")
          public BooleanOption includeCACert = BooleanOption.YES;

          /**
           * The server certificates store. Can be set via "-srvcerts" option. The default value is
           * "srv-certs.store".
           */
          @Option(name="-srvcerts",
                  aliases={"--server-certs-store"},
                  usage="The server certificates store")
          public String serverCertsStore = SERVER_CERTS_STORE;

          /**
           * Indicates if the root CA private key will be saved in an external key-store. Can be set
           * via "-ECAK" option.
           */
          @Option(name="-ECAK",
                  aliases={"--export-ca-key"},
                  usage="Save the root CA private key in an external key store")
          public BooleanOption exportCAPrivateKey = BooleanOption.YES;

          /**
           * The CA private key store. Can be set via "-cakeys" option. The default value is
           * "root-ca-pk.store".
           */
          @Option(name="-cakeys",
                  aliases={"--root-private-keys-store"},
                  usage="The root CA private keys store")
          public String rootPrivateKeysStore = ROOT_CA_PRIVATE_KEYS_STORE;

          /**
           * Indicates if the account private keys will be saved in a common external key-store.
           * Can be set via "-UCAS" option.
           */
          @Option(name="-UCAS",
                  aliases={"--use-common-account-store"},
                  usage="Save the account private keys in the same key store")
          public BooleanOption useCommonAccountKeyStore = BooleanOption.NO;

          /**
           * The account private keys common key store. Can be set via "-CAS" option.
           */
          @Option(name="-cas",
                  aliases={"--common-account-store"},
                  usage="The account private keys common key store")
          public String commonAccountKeyStore;

          /**
           * Indicates if the server private keys will be saved in a common external key-store.
           * Can be set via "-UCSS" option.
           */
          @Option(name="-UCSS",
                  aliases={"--use-common-server-store"},
                  usage="Save the server private keys in the same key store")
          public BooleanOption useCommonServerKeyStore = BooleanOption.NO;

          /**
           * The server private keys common key store. Can be set via "-CSS" option.
           */
          @Option(name="-css",
                  aliases={"--common-server-store"},
                  usage="The server private keys common key store")
          public String commonServerKeyStore;

          /**
           * The RSA key size. Can be set via "-rsa" option. The default value is 2048.
           */
          @Option(name="-rsa", aliases={"--rsa-key-size"}, usage="The RSA key size.")
          public Integer rsaKeySize = RSA_KEY_LENGTH_DEFAULT_VALUE;

          /**
           * The RSA public exponent. Can be set via "-exp" option. The default value is 65337.
           */
          @Option(name="-exp", aliases={"--rsa-public-exponent"}, usage="The RSA public exponent.")
          public BigInteger rsaPublicExponent = RSA_PUBLIC_EXPONENT_DEFAULT_VALUE;

          /**
           * The target alias for process or user account to generate or to rebuild its public and
           * private certificates.
           */
          @Option(name="-bal",
                  aliases="--build-alias-certs",
                  depends={"-C"},
                  usage="To generate public and private certificates for the target alias.")
          public String buildAliasCerts;

          /**
           * The P2J server identifier.
           */
          @Option(name="-sid",
                  aliases="--server-id",
                  usage="The P2J server identifier.")
          public String serverId;

          /**
           * The web certificate alias.
           */
          @Option(name="-walias",
                  aliases="--web-alias",
                  usage="The web certificate alias.")
          public String webAlias;

          /**
           * The ACME client host IP address that is accessible for Let's Encrypt ACME server.
           */
          @Option(name="-host",
                  aliases="--lets-encrypt-client-host",
                  depends={"-acme"},
                  forbids={"-C", "-L", "-bal"},
                  usage="The ACME client host IP address.")
          public String host;

          /**
           * The ACME client port that is redirected 443 port accessible for Let's Encrypt ACME server.
           */
          @Option(name="-port",
                  depends={"-acme"},
                  aliases="--lets-encrypt-client-port",
                  forbids={"-C", "-L", "-bal"},
                  usage="The ACME client port that is redirected 443 port accessible for Let's " +
                        "Encrypt ACME server.")
          public int port;

          /**
           * The list of requested domains separated by spaces that is similar to this example,
           * "test1.acme.com test2.acme.com test3.acme.com". The wild cards are not supported.
           */
          @Option(name="-domains",
                  depends={"-acme"},
                  forbids={"-C", "-L", "-bal"},
                  usage="The requested domains enclosed in one string within double quoters and " +
                        "separated by spaces.")
          public String domains;

          /**
           * The registration contact information. Can be set via "-contact" option,
           * "-contact admin@acme.com". Let's Encrypt doesn't support API to add new contact after
           * the ACME account has been registered.
           */
          @Option(name="-contact",
                  depends={"-acme"},
                  forbids={"-C", "-L", "-bal"},
                  usage="The registration contact information.")
          public String contact;
       }
    }