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;
   }
}