SelfSignedCertGen.java

/*
** Module   : SelfSignedCertGen.java
** Abstract : generates a root CA, a new private key and a matching self-signed certificate
**
** Copyright (c) 2016-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------Description----------------------------------
** 001 GES 20160118 First version based on SSLCertGenUtil.
** 002 TJD 20220504          Java 11 compatibility minor changes
** 003 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
*/
/*
** 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 com.goldencode.util.*;

/**
 * Generate a new root CA and a server certificates and save them and the associated private keys
 * in external key stores.  This utility does not integrate with the directory at all.  It is
 * a helper to create a single certificate that can be used for a standalone web server.
 */
public class SelfSignedCertGen
{
   /** Logger */
   private static final CentralLogger LOG = CentralLogger.get(SelfSignedCertGen.class);
   
   /** Valid entries for yes/no options. */
   private static final String[] YES_NO = { "yes", "no" };
   
   /** Read data from the standard input. */
   private final BufferedReader reader;

   /** Map identifying the company attributes. */
   private final Map<String, String> company = new HashMap<>();
   
   /** 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;

   /**
    * 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;
   
   /** Server alias. */
   private String serverAlias = null;
   
   /** The master password to encrypt all private keys, in the directory. */
   private String masterPassword = null;

   /**
    * Create a new utility instance.
    * 
    * @throws   SSLCertGenException
    *           If the utility could not be instantiated.
    */
   public SelfSignedCertGen()
   throws IOException,
          SSLCertGenException
   {
      Console console = System.console();
      
      if (console != null)
      {
         reader = new BufferedReader(console.reader());
      }
      else
      {
         reader = new BufferedReader(new InputStreamReader(System.in));
      }
      
      // hard-coded to Bouncy Castle
      this.factory = new BCCertFactory();
      
      // configure the factory and read the user-specified options for generation
      readConfiguration();
      
      // we are using 2 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)

      trustCertStore = createEmptyStore();
      trustKeyStore  = createEmptyStore();
   }
   
   /**
    * Main method to (re)generate the root CA, peer certificates and private keys.
    * 
    * @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 IOException,
          SSLCertGenException
   {
      generateRootCA();
      generateServerCertificate();
      
      masterPassword = createAES256BitKey();
      
      saveServerPrivateKey();
      saveServerCertificates();
      saveRootCAPrivateKey();
   }
   
   /**
    * 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 = String.format("%s-private-key-root-ca.store", rootCAAlias);

      try
      {
         String kepass = keyEntryPasswords.get(rootCAAlias);
         Key rootKey = this.trustKeyStore.getKey(rootCAAlias, kepass.toCharArray());
         Certificate rootCert = this.trustCertStore.getCertificate(rootCAAlias);
         
         KeyStore store = createEmptyStore();
         store.setKeyEntry(rootCAAlias, rootKey, kepass.toCharArray(),
                           new Certificate[] { rootCert });
         
         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
      String answer = readOption("Include the root CA certificate, to use this store as a " +
                                 "trust store (yes/no)? ", YES_NO);

      boolean saveRootCA = "yes".equalsIgnoreCase(answer);
      
      String file = String.format("%s-cert-trust.store", serverAlias);
      
      try
      {
         KeyStore store = createEmptyStore();
         Certificate cert = trustCertStore.getCertificate(serverAlias);
         store.setCertificateEntry(serverAlias, cert);
         
         if (saveRootCA)
         {
            store.setCertificateEntry(rootCAAlias, trustCertStore.getCertificate(rootCAAlias));
         }
         
         String pass = RandomWordGenerator.create();
         store.store(new FileOutputStream(file), pass.toCharArray());
         
         String msg = "The server certificate for %s was saved in the [%s] key store, " +
                      "using password [%s].";
         System.out.println(String.format(msg, serverAlias, file, pass));
      }
      catch (KeyStoreException | NoSuchAlgorithmException | CertificateException e)
      {
         throw new SSLCertGenException(e);
      }
   }
   
   /**
    * Save the private keys in external key store(s).
    * 
    * @throws   IOException
    *           If standard input can not be accessed.
    * @throws   SSLCertGenException
    *           In case of problems during key store access.
    */
   private void saveServerPrivateKey()
   throws IOException,
          SSLCertGenException
   {
      KeyStore pkStore = createEmptyStore();
      String   pkFile  = String.format("%s-private-key.store", serverAlias);
      
      KeyStore certStore = trustCertStore;
      KeyStore keyStore  = trustKeyStore;
      
      try
      {
         String      password = keyEntryPasswords.get(serverAlias);
         Key         key      = keyStore.getKey(serverAlias, password.toCharArray());
         Certificate cert     = certStore.getCertificate(serverAlias);
         Certificate rootCert = trustCertStore.getCertificate(rootCAAlias);
         
         pkStore.setKeyEntry(serverAlias, key, password.toCharArray(), 
                             new Certificate[] { cert, rootCert });
         
         String ksPass = RandomWordGenerator.create();
         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, serverAlias, pkFile, ksPass, password));
      }
      catch (UnrecoverableKeyException |
             KeyStoreException         |
             NoSuchAlgorithmException  |
             CertificateException e)
      {
         throw new SSLCertGenException(e);
      }
   }
   
   /**
    * Generate peer certificate for the server.
    * 
    * @throws    SSLCertGenException
    *            If the SSL certificates could not be generated.
    */
   private void generateServerCertificate()
   throws SSLCertGenException
   {
      String commonName = serverAlias + " server";
         
      System.out.println(String.format("Generating certificate for server with alias [%s]...",
                                       serverAlias));
      
      String password = factory.generateCertificate(serverAlias,
                                                    validity, 
                                                    commonName,
                                                    company,
                                                    trustCertStore, 
                                                    trustKeyStore);

      keyEntryPasswords.put(serverAlias, password);
   }
   
   /**
    * Generate the root CA.
    * 
    * @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
   {
      final String msg = "Generating root CA using [%s] alias...";
      System.out.println(String.format(msg, rootCAAlias));
      
      String password = factory.generateRootCA(rootCAAlias,
                                               validity,
                                               rootCAAlias + " - root CA authority",
                                               company,
                                               trustCertStore,
                                               trustKeyStore);
      
      keyEntryPasswords.put(rootCAAlias, password);
   }
     
   /**
    * Read the existing configuration 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 readConfiguration()
   throws IOException,
          SSLCertGenException
   {
      Map<String, String> mandatoryKeys = factory.getMandatorySubjectFields();
      
      Integer    rsaKeyStrength    = null;
      BigInteger rsaPublicExponent = null;
      
      // read the root CA alias
      while (rootCAAlias == null || rootCAAlias.length() == 0)
      {
         rootCAAlias = readLine("Enter the root CA alias: ");
      }
      
      // read the server alias
      while (serverAlias == null || serverAlias.length() == 0)
      {
         serverAlias = readLine("Enter the server alias: ");
         
         if (!serverAlias.equals(rootCAAlias))
         {
            break;
         }
         
         System.out.println("Server alias cannot be the same as the root CA alias!");
         serverAlias = null;
      }
      
      // get user input for the mandatory nodes
      for (String oid : mandatoryKeys.keySet())
      {
         String txt = mandatoryKeys.get(oid);
         
         // read the value
         String val = readLine(String.format("%s [%s]: ", txt, oid));
         
         // store in the map
         company.put(oid, val);
         
         // report the results
         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;
            }
         }
         
         System.out.println(String.format("Using %d years for validity.", validity));
      }
      
      // get the key strength
      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)
            {
               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 (rsaPublicExponent == null)
      {
         String line = readLine("Enter RSA public exponent, blank for default (65337): ");
         if (line.length() > 0)
         {
            rsaPublicExponent = new BigInteger(line);
            System.out.println(String.format("Using %s for RSA public exponent.", 
                                             rsaPublicExponent.toString()));
         }
         else
         {
            rsaPublicExponent = new BigInteger("65337");
         }
      }
      
      factory.init(rsaKeyStrength, rsaPublicExponent);
   }

   /**
    * 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
   {
      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.
    */
   private 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.
    * 
    * @return   See above.
    */
   private String createAES256BitKey()
   {
      return RandomWordGenerator.create(32, 8, 4);
   }
   
   /**
    * Command line driver.
    * 
    * @param    args
    *           Application command line parameters. File name is the only one expected.
    *
    * @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 IOException,
          SSLCertGenException
   {
      try
      {
         SelfSignedCertGen gen = new SelfSignedCertGen();
         gen.generate();
      }
      
      catch (Throwable thr)
      {
         LOG.severe("", thr);
      }
   }
}