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