/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.apache.jorphan.exec;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.SystemUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Utilities for working with Java keytool
*/
public class KeyToolUtils {
private static final Logger log = LoggerFactory.getLogger(KeyToolUtils.class);
// The DNAME which is used if none is provided
private static final String DEFAULT_DNAME = "cn=JMeter Proxy (DO NOT TRUST)"; // $NON-NLS-1$
// N.B. It seems that Opera needs a chain in order to accept server keys signed by the intermediate CA
// Opera does not seem to like server keys signed by the root (self-signed) cert.
private static final String DNAME_ROOT_CA_KEY;
private static final String KEYTOOL = "keytool";
/** Name of property that can be used to override the default keytool location */
private static final String KEYTOOL_DIRECTORY = "keytool.directory"; // $NON-NLS-1$
private static final String DNAME_INTERMEDIATE_CA_KEY = "cn=DO NOT INSTALL THIS CERTIFICATE (JMeter Intermediate CA)"; // $NON-NLS-1$
public static final String ROOT_CACERT_CRT_PFX = "ApacheJMeterTemporaryRootCA"; // $NON-NLS-1$ (do not change)
private static final String ROOT_CACERT_CRT = ROOT_CACERT_CRT_PFX + ".crt"; // $NON-NLS-1$ (Firefox and Windows)
private static final String ROOT_CACERT_USR = ROOT_CACERT_CRT_PFX + ".usr"; // $NON-NLS-1$ (Opera)
private static final String ROOTCA_ALIAS = ":root_ca:"; // $NON-NLS-1$
private static final String INTERMEDIATE_CA_ALIAS = ":intermediate_ca:"; // $NON-NLS-1$
/**
* Where to find the keytool application.
* If <code>null</code>, then keytool cannot be found.
*/
private static final String KEYTOOL_PATH;
static {
StringBuilder sb = new StringBuilder();
sb.append("CN=_ DO NOT INSTALL unless this is your certificate (JMeter root CA)"); // $NON-NLS-1$
String userName = System.getProperty("user.name"); // $NON-NLS-1$
userName = userName.replace('\\','/'); // Backslash is special (Bugzilla 56178)
addElement(sb, "OU=Username: ", userName); // $NON-NLS-1$
addElement(sb, "C=", System.getProperty("user.country")); // $NON-NLS-1$ $NON-NLS-2$
DNAME_ROOT_CA_KEY = sb.toString();
// Try to find keytool application
// N.B. Cannot use JMeter property from jorphan jar.
final String keytoolDir = System.getProperty(KEYTOOL_DIRECTORY);
String keytoolPath; // work field
if (keytoolDir != null) {
keytoolPath = new File(new File(keytoolDir), KEYTOOL).getPath();
if (!checkKeytool(keytoolPath)) {
log.error("Cannot find keytool using property " + KEYTOOL_DIRECTORY + "=" + keytoolDir);
keytoolPath = null; // don't try anything else if the property is provided
}
} else {
keytoolPath = KEYTOOL;
if (!checkKeytool(keytoolPath)) { // Not found on PATH, check Java Home
File javaHome = SystemUtils.getJavaHome();
if (javaHome != null) {
keytoolPath = new File(new File(javaHome, "bin"), KEYTOOL).getPath(); // $NON-NLS-1$
if (!checkKeytool(keytoolPath)) {
keytoolPath = null;
}
} else {
keytoolPath = null;
}
}
}
if (keytoolPath == null) {
log.error("Unable to find keytool application. Check PATH or define system property " + KEYTOOL_DIRECTORY);
} else {
log.info("keytool found at '" + keytoolPath + "'");
}
KEYTOOL_PATH = keytoolPath;
}
private KeyToolUtils() {
// not instantiable
}
private static void addElement(StringBuilder sb, String prefix, String value) {
if (value != null) {
sb.append(", ");
sb.append(prefix);
sb.append(value);
}
}
/**
* Generate a self-signed keypair using the algorithm "RSA".
*
* @param keystore the keystore; if it already contains the alias the command will fail
* @param alias the alias to use, not null
* @param password the password to use for the store and the key
* @param validity the validity period in days, greater than 0
* @param dname the <em>distinguished name</em> value, if omitted use "cn=JMeter Proxy (DO NOT TRUST)"
* @param ext if not null, the extension (-ext) to add (e.g. "bc:c").
*
* @throws IOException if keytool was not configured or running keytool application fails
*/
public static void genkeypair(final File keystore, String alias, final String password, int validity, String dname, String ext)
throws IOException {
final File workingDir = keystore.getParentFile();
final SystemCommand nativeCommand = new SystemCommand(workingDir, null);
final List<String> arguments = new ArrayList<>();
arguments.add(getKeyToolPath());
arguments.add("-genkeypair"); // $NON-NLS-1$
arguments.add("-alias"); // $NON-NLS-1$
arguments.add(alias);
arguments.add("-dname"); // $NON-NLS-1$
arguments.add(dname == null ? DEFAULT_DNAME : dname);
arguments.add("-keyalg"); // $NON-NLS-1$
arguments.add("RSA"); // $NON-NLS-1$
arguments.add("-keystore"); // $NON-NLS-1$
arguments.add(keystore.getName());
arguments.add("-storepass"); // $NON-NLS-1$
arguments.add(password);
arguments.add("-keypass"); // $NON-NLS-1$
arguments.add(password);
arguments.add("-validity"); // $NON-NLS-1$
arguments.add(Integer.toString(validity));
if (ext != null) {
arguments.add("-ext"); // $NON-NLS-1$
arguments.add(ext);
}
try {
int exitVal = nativeCommand.run(arguments);
if (exitVal != 0) {
throw new IOException(" >> " + nativeCommand.getOutResult().trim() + " <<"
+ "\nCommand failed, code: " + exitVal
+ "\n'" + formatCommand(arguments)+"'");
}
} catch (InterruptedException e) { // NOSONAR
throw new IOException("Command was interrupted\n" + nativeCommand.getOutResult(), e);
}
}
/**
* Formats arguments
* @param arguments
* @return String command line
*/
private static String formatCommand(List<String> arguments) {
StringBuilder builder = new StringBuilder();
boolean redact = false; // whether to redact next parameter
for (String string : arguments) {
final boolean quote = string.contains(" ");
if (quote) {
builder.append("\"");
}
builder.append(redact ? "{redacted}" : string);
if (quote) {
builder.append("\"");
}
builder.append(" ");
redact = "-storepass".equals(string) || "-keypass".equals(string);
}
if (!arguments.isEmpty()) {
builder.setLength(builder.length() - 1); // trim trailing space
}
return builder.toString();
}
/**
* Creates a self-signed Root CA certificate and an intermediate CA certificate
* (signed by the Root CA certificate) that can be used to sign server certificates.
* The Root CA certificate file is exported to the same directory as the keystore
* in formats suitable for Firefox/Chrome/IE (.crt) and Opera (.usr).
*
* @param keystore the keystore in which to store everything
* @param password the password for keystore and keys
* @param validity the validity period in days, must be greater than 0
*
* @throws IOException if keytool was not configured, running keytool application failed or copying the keys failed
*/
public static void generateProxyCA(File keystore, String password, int validity) throws IOException {
File caCertCrt = new File(ROOT_CACERT_CRT);
File caCertUsr = new File(ROOT_CACERT_USR);
boolean fileExists = false;
if (!keystore.delete() && keystore.exists()) {
log.warn("Problem deleting the keystore '" + keystore + "'");
fileExists = true;
}
if (!caCertCrt.delete() && caCertCrt.exists()) {
log.warn("Problem deleting the certificate file '" + caCertCrt + "'");
fileExists = true;
}
if (!caCertUsr.delete() && caCertUsr.exists()) {
log.warn("Problem deleting the certificate file '" + caCertUsr + "'");
fileExists = true;
}
if (fileExists) {
log.warn("If problems occur when recording SSL, delete the files manually and retry.");
}
// Create the self-signed keypairs
KeyToolUtils.genkeypair(keystore, ROOTCA_ALIAS, password, validity, DNAME_ROOT_CA_KEY, "bc:c");
KeyToolUtils.genkeypair(keystore, INTERMEDIATE_CA_ALIAS, password, validity, DNAME_INTERMEDIATE_CA_KEY, "bc:c");
// Create cert for CA using root
ByteArrayOutputStream certReqOut = new ByteArrayOutputStream();
// generate the request
KeyToolUtils.keytool("-certreq", keystore, password, INTERMEDIATE_CA_ALIAS, null, certReqOut);
// generate the certificate and store in output file
InputStream certReqIn = new ByteArrayInputStream(certReqOut.toByteArray());
ByteArrayOutputStream genCertOut = new ByteArrayOutputStream();
KeyToolUtils.keytool("-gencert", keystore, password, ROOTCA_ALIAS, certReqIn, genCertOut, "-ext", "BC:0");
// import the signed CA cert into the store (root already there) - both are needed to sign the domain certificates
InputStream genCertIn = new ByteArrayInputStream(genCertOut.toByteArray());
KeyToolUtils.keytool("-importcert", keystore, password, INTERMEDIATE_CA_ALIAS, genCertIn, null);
// Export the Root CA for Firefox/Chrome/IE
KeyToolUtils.keytool("-exportcert", keystore, password, ROOTCA_ALIAS, null, null, "-rfc", "-file", ROOT_CACERT_CRT);
// Copy for Opera
if(caCertCrt.exists() && caCertCrt.canRead()) {
FileUtils.copyFile(caCertCrt, caCertUsr);
} else {
log.warn("Failed creating "+caCertCrt.getAbsolutePath()+", check 'keytool' utility in path is available and points to a JDK >= 7");
}
}
/**
* Create a host certificate signed with the CA certificate.
*
* @param keystore the keystore to use
* @param password the password to use for the keystore and keys
* @param host the host, e.g. jmeter.apache.org or *.apache.org; also used as the alias
* @param validity the validity period for the generated keypair
*
* @throws IOException if keytool was not configured or running keytool application failed
*
*/
public static void generateHostCert(File keystore, String password, String host, int validity) throws IOException {
// generate the keypair for the host
generateSignedCert(keystore, password, validity,
host, // alias
host); // subject
}
private static void generateSignedCert(File keystore, String password,
int validity, String alias, String subject) throws IOException {
String dname = "cn=" + subject + ", o=JMeter Proxy (TEMPORARY TRUST ONLY)";
KeyToolUtils.genkeypair(keystore, alias, password, validity, dname, null);
//rem generate cert for DOMAIN using CA and import it
// get the certificate request
ByteArrayOutputStream certReqOut = new ByteArrayOutputStream();
KeyToolUtils.keytool("-certreq", keystore, password, alias, null, certReqOut);
// create the certificate
//rem ku:c=dig,keyE means KeyUsage:criticial=digitalSignature,keyEncipherment
InputStream certReqIn = new ByteArrayInputStream(certReqOut.toByteArray());
ByteArrayOutputStream certOut = new ByteArrayOutputStream();
KeyToolUtils.keytool("-gencert", keystore, password, INTERMEDIATE_CA_ALIAS, certReqIn, certOut, "-ext", "ku:c=dig,keyE");
// import the certificate
InputStream certIn = new ByteArrayInputStream(certOut.toByteArray());
KeyToolUtils.keytool("-importcert", keystore, password, alias, certIn, null, "-noprompt");
}
/**
* List the contents of a keystore
*
* @param keystore
* the keystore file
* @param storePass
* the keystore password
* @return the output from the command "keytool -list -v"
* @throws IOException
* if keytool was not configured or running keytool application
* failed
*/
public static String list(final File keystore, final String storePass) throws IOException {
final File workingDir = keystore.getParentFile();
final SystemCommand nativeCommand = new SystemCommand(workingDir, null);
final List<String> arguments = new ArrayList<>();
arguments.add(getKeyToolPath());
arguments.add("-list"); // $NON-NLS-1$
arguments.add("-v"); // $NON-NLS-1$
arguments.add("-keystore"); // $NON-NLS-1$
arguments.add(keystore.getName());
arguments.add("-storepass"); // $NON-NLS-1$
arguments.add(storePass);
runNativeCommand(nativeCommand, arguments);
return nativeCommand.getOutResult();
}
/**
* @param nativeCommand {@link SystemCommand}
* @param arguments {@link List}
*/
private static void runNativeCommand(SystemCommand nativeCommand, List<String> arguments) throws IOException {
try {
int exitVal = nativeCommand.run(arguments);
if (exitVal != 0) {
throw new IOException("Command failed, code: " + exitVal + "\n" + nativeCommand.getOutResult());
}
} catch (InterruptedException e) { // NOSONAR
throw new IOException("Command was interrupted\n" + nativeCommand.getOutResult(), e);
}
}
/**
* Returns a list of the CA aliases that should be in the keystore.
*
* @return the aliases that are used for the keystore
*/
public static String[] getCAaliases() {
return new String[]{ROOTCA_ALIAS, INTERMEDIATE_CA_ALIAS};
}
/**
* Get the root CA alias; needed to check the serial number and fingerprint
*
* @return the alias
*/
public static String getRootCAalias() {
return ROOTCA_ALIAS;
}
/**
* Helper method to simplify chaining keytool commands.
*
* @param command
* the command, not null
* @param keystore
* the keystore, not nill
* @param password
* the password used for keystore and key, not null
* @param alias
* the alias, not null
* @param input
* where to source input, may be null
* @param output
* where to send output, may be null
* @param parameters
* additional parameters to the command, may be null
* @throws IOException
* if keytool is not configured or running it failed
*/
private static void keytool(String command, File keystore, String password, String alias,
InputStream input, OutputStream output, String ... parameters)
throws IOException {
final File workingDir = keystore.getParentFile();
final SystemCommand nativeCommand =
new SystemCommand(workingDir, 0L, 0, null, input, output, null);
final List<String> arguments = new ArrayList<>();
arguments.add(getKeyToolPath());
arguments.add(command);
arguments.add("-keystore"); // $NON-NLS-1$
arguments.add(keystore.getName());
arguments.add("-storepass"); // $NON-NLS-1$
arguments.add(password);
arguments.add("-keypass"); // $NON-NLS-1$
arguments.add(password);
arguments.add("-alias"); // $NON-NLS-1$
arguments.add(alias);
Collections.addAll(arguments, parameters);
runNativeCommand(nativeCommand, arguments);
}
/**
* @return flag whether {@link KeyToolUtils#KEYTOOL_PATH KEYTOOL_PATH} is
* configured (is not <code>null</code>)
*/
public static boolean haveKeytool() {
return KEYTOOL_PATH != null;
}
/**
* @return path to keytool binary
* @throws IOException
* when {@link KeyToolUtils#KEYTOOL_PATH KEYTOOL_PATH} is
* <code>null</code>
*/
private static String getKeyToolPath() throws IOException {
if (KEYTOOL_PATH == null) {
throw new IOException("keytool application cannot be found");
}
return KEYTOOL_PATH;
}
/**
* Check if keytool can be found
* @param keytoolPath the path to check
*/
private static boolean checkKeytool(String keytoolPath) {
final SystemCommand nativeCommand = new SystemCommand(null, null);
final List<String> arguments = new ArrayList<>();
arguments.add(keytoolPath);
arguments.add("-help"); // $NON-NLS-1$
try {
int status = nativeCommand.run(arguments);
if (log.isDebugEnabled()) {
log.debug("checkKeyTool:status=" + status);
log.debug(nativeCommand.getOutResult());
}
/*
* Some implementations of keytool return status 1 for -help
* MacOS/Java 7 returns 2 if it cannot find keytool
*/
return status == 0 || status == 1; // TODO this is rather fragile
} catch (IOException ioe) {
log.info("Exception checking for keytool existence, will return false, try another way.");
log.debug("Exception is: ", ioe);
return false;
} catch (InterruptedException e) { // NOSONAR
log.error("Command was interrupted\n" + nativeCommand.getOutResult(), e);
return false;
}
}
}