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