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