Usability enhancements for certificate generation (elastic/x-pack-elasticsearch#2561)

This commit adds a new `certutil` command and deprecates the `certgen` command.
 
The new certuil consists of sub commands that are (by default) are simpler to use than the old monolithic command, but still support all the previous behaviours.

Original commit: elastic/x-pack-elasticsearch@3f57687da9
This commit is contained in:
Tim Vernum 2017-10-30 13:08:31 +11:00 committed by GitHub
parent ba29971323
commit 0c7caabea1
10 changed files with 2630 additions and 490 deletions

View File

@ -14,5 +14,5 @@ exec \
-Des.path.home="$ES_HOME" \ -Des.path.home="$ES_HOME" \
-Des.path.conf="$ES_PATH_CONF" \ -Des.path.conf="$ES_PATH_CONF" \
-cp "$ES_CLASSPATH" \ -cp "$ES_CLASSPATH" \
org.elasticsearch.xpack.ssl.CertificateTool \ org.elasticsearch.xpack.ssl.CertificateGenerateTool \
"$@" "$@"

Binary file not shown.

View File

@ -0,0 +1,18 @@
#!/bin/bash
# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
# or more contributor license agreements. Licensed under the Elastic License;
# you may not use this file except in compliance with the Elastic License.
source "`dirname "$0"`"/../elasticsearch-env
source "`dirname "$0"`"/x-pack-env
exec \
"$JAVA" \
$ES_JAVA_OPTS \
-Des.path.home="$ES_HOME" \
-Des.path.conf="$ES_PATH_CONF" \
-cp "$ES_CLASSPATH" \
org.elasticsearch.xpack.ssl.CertificateTool \
"$@"

Binary file not shown.

View File

@ -22,6 +22,7 @@ import java.net.SocketException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.security.Key;
import java.security.KeyPair; import java.security.KeyPair;
import java.security.KeyPairGenerator; import java.security.KeyPairGenerator;
import java.security.KeyStore; import java.security.KeyStore;
@ -35,12 +36,18 @@ import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory; import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.bouncycastle.asn1.ASN1Encodable; import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.ASN1ObjectIdentifier;
@ -93,7 +100,8 @@ public class CertUtils {
private static final int SERIAL_BIT_LENGTH = 20 * 8; private static final int SERIAL_BIT_LENGTH = 20 * 8;
static final BouncyCastleProvider BC_PROV = new BouncyCastleProvider(); static final BouncyCastleProvider BC_PROV = new BouncyCastleProvider();
private CertUtils() {} private CertUtils() {
}
/** /**
* Resolves a path with or without an {@link Environment} as we may be running in a transport client where we do not have access to * Resolves a path with or without an {@link Environment} as we may be running in a transport client where we do not have access to
@ -107,16 +115,36 @@ public class CertUtils {
return PathUtils.get(path).normalize(); return PathUtils.get(path).normalize();
} }
/**
* Creates a {@link KeyStore} from a PEM encoded certificate and key file
*/
static KeyStore getKeyStoreFromPEM(Path certificatePath, Path keyPath, char[] keyPassword)
throws IOException, CertificateException, KeyStoreException, NoSuchAlgorithmException {
final PrivateKey key;
try (Reader reader = Files.newBufferedReader(keyPath, StandardCharsets.UTF_8)) {
key = CertUtils.readPrivateKey(reader, () -> keyPassword);
}
final Certificate[] certificates = readCertificates(Collections.singletonList(certificatePath));
return getKeyStore(certificates, key, keyPassword);
}
/** /**
* Returns a {@link X509ExtendedKeyManager} that is built from the provided private key and certificate chain * Returns a {@link X509ExtendedKeyManager} that is built from the provided private key and certificate chain
*/ */
public static X509ExtendedKeyManager keyManager(Certificate[] certificateChain, PrivateKey privateKey, char[] password) public static X509ExtendedKeyManager keyManager(Certificate[] certificateChain, PrivateKey privateKey, char[] password)
throws NoSuchAlgorithmException, UnrecoverableKeyException, KeyStoreException, IOException, CertificateException { throws NoSuchAlgorithmException, UnrecoverableKeyException, KeyStoreException, IOException, CertificateException {
KeyStore keyStore = getKeyStore(certificateChain, privateKey, password);
return keyManager(keyStore, password, KeyManagerFactory.getDefaultAlgorithm());
}
private static KeyStore getKeyStore(Certificate[] certificateChain, PrivateKey privateKey, char[] password)
throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException {
KeyStore keyStore = KeyStore.getInstance("jks"); KeyStore keyStore = KeyStore.getInstance("jks");
keyStore.load(null, null); keyStore.load(null, null);
// password must be non-null for keystore... // password must be non-null for keystore...
keyStore.setKeyEntry("key", privateKey, password, certificateChain); keyStore.setKeyEntry("key", privateKey, password, certificateChain);
return keyManager(keyStore, password, KeyManagerFactory.getDefaultAlgorithm()); return keyStore;
} }
/** /**
@ -137,12 +165,19 @@ public class CertUtils {
/** /**
* Creates a {@link X509ExtendedTrustManager} based on the provided certificates * Creates a {@link X509ExtendedTrustManager} based on the provided certificates
*
* @param certificates the certificates to trust * @param certificates the certificates to trust
* @return a trust manager that trusts the provided certificates * @return a trust manager that trusts the provided certificates
*/ */
public static X509ExtendedTrustManager trustManager(Certificate[] certificates) public static X509ExtendedTrustManager trustManager(Certificate[] certificates)
throws NoSuchAlgorithmException, UnrecoverableKeyException, KeyStoreException, IOException, CertificateException { throws NoSuchAlgorithmException, UnrecoverableKeyException, KeyStoreException, IOException, CertificateException {
assert certificates != null : "Cannot create trust manager with null certificates"; KeyStore store = trustStore(certificates);
return trustManager(store, TrustManagerFactory.getDefaultAlgorithm());
}
static KeyStore trustStore(Certificate[] certificates)
throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException {
assert certificates != null : "Cannot create trust store with null certificates";
KeyStore store = KeyStore.getInstance("jks"); KeyStore store = KeyStore.getInstance("jks");
store.load(null, null); store.load(null, null);
int counter = 0; int counter = 0;
@ -150,25 +185,32 @@ public class CertUtils {
store.setCertificateEntry("cert" + counter, certificate); store.setCertificateEntry("cert" + counter, certificate);
counter++; counter++;
} }
return trustManager(store, TrustManagerFactory.getDefaultAlgorithm()); return store;
} }
/** /**
* Loads the truststore and creates a {@link X509ExtendedTrustManager} * Loads the truststore and creates a {@link X509ExtendedTrustManager}
* @param trustStorePath the path to the truststore *
* @param trustStorePassword the password to the truststore * @param trustStorePath the path to the truststore
* @param trustStorePassword the password to the truststore
* @param trustStoreAlgorithm the algorithm to use for the truststore * @param trustStoreAlgorithm the algorithm to use for the truststore
* @param env the environment to use for file resolution. May be {@code null} * @param env the environment to use for file resolution. May be {@code null}
* @return a trust manager with the trust material from the store * @return a trust manager with the trust material from the store
*/ */
public static X509ExtendedTrustManager trustManager(String trustStorePath, String trustStoreType, char[] trustStorePassword, public static X509ExtendedTrustManager trustManager(String trustStorePath, String trustStoreType, char[] trustStorePassword,
String trustStoreAlgorithm, @Nullable Environment env) String trustStoreAlgorithm, @Nullable Environment env)
throws NoSuchAlgorithmException, UnrecoverableKeyException, KeyStoreException, IOException, CertificateException { throws NoSuchAlgorithmException, UnrecoverableKeyException, KeyStoreException, IOException, CertificateException {
try (InputStream in = Files.newInputStream(resolvePath(trustStorePath, env))) { KeyStore trustStore = readKeyStore(resolvePath(trustStorePath, env), trustStoreType, trustStorePassword);
KeyStore trustStore = KeyStore.getInstance(trustStoreType); return trustManager(trustStore, trustStoreAlgorithm);
assert trustStorePassword != null; }
trustStore.load(in, trustStorePassword);
return trustManager(trustStore, trustStoreAlgorithm); static KeyStore readKeyStore(Path path, String type, char[] password)
throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException {
try (InputStream in = Files.newInputStream(path)) {
KeyStore store = KeyStore.getInstance(type);
assert password != null;
store.load(in, password);
return store;
} }
} }
@ -182,7 +224,7 @@ public class CertUtils {
TrustManager[] trustManagers = tmf.getTrustManagers(); TrustManager[] trustManagers = tmf.getTrustManagers();
for (TrustManager trustManager : trustManagers) { for (TrustManager trustManager : trustManagers) {
if (trustManager instanceof X509ExtendedTrustManager) { if (trustManager instanceof X509ExtendedTrustManager) {
return (X509ExtendedTrustManager) trustManager ; return (X509ExtendedTrustManager) trustManager;
} }
} }
throw new IllegalStateException("failed to find a X509ExtendedTrustManager"); throw new IllegalStateException("failed to find a X509ExtendedTrustManager");
@ -190,16 +232,22 @@ public class CertUtils {
/** /**
* Reads the provided paths and parses them into {@link Certificate} objects * Reads the provided paths and parses them into {@link Certificate} objects
* @param certPaths the paths to the PEM encoded certificates *
* @param certPaths the paths to the PEM encoded certificates
* @param environment the environment to resolve files against. May be {@code null} * @param environment the environment to resolve files against. May be {@code null}
* @return an array of {@link Certificate} objects * @return an array of {@link Certificate} objects
*/ */
public static Certificate[] readCertificates(List<String> certPaths, @Nullable Environment environment) public static Certificate[] readCertificates(List<String> certPaths, @Nullable Environment environment)
throws CertificateException, IOException { throws CertificateException, IOException {
final List<Path> resolvedPaths = certPaths.stream().map(p -> resolvePath(p, environment)).collect(Collectors.toList());
return readCertificates(resolvedPaths);
}
static Certificate[] readCertificates(List<Path> certPaths) throws CertificateException, IOException {
List<Certificate> certificates = new ArrayList<>(certPaths.size()); List<Certificate> certificates = new ArrayList<>(certPaths.size());
CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
for (String path : certPaths) { for (Path path : certPaths) {
try (Reader reader = Files.newBufferedReader(resolvePath(path, environment), StandardCharsets.UTF_8)) { try (Reader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
readCertificates(reader, certificates, certFactory); readCertificates(reader, certificates, certFactory);
} }
} }
@ -280,6 +328,30 @@ public class CertUtils {
return privateKeyInfo; return privateKeyInfo;
} }
/**
* Read all certificate-key pairs from a PKCS#12 container.
*
* @param path The path to the PKCS#12 container file.
* @param password The password for the container file
* @param keyPassword A supplier for the password for each key. The key alias is supplied as an argument to the function, and it should
* return the password for that key. If it returns {@code null}, then the key-pair for that alias is not read.
*/
static Map<Certificate, Key> readPkcs12KeyPairs(Path path, char[] password, Function<String, char[]> keyPassword, Environment env)
throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, UnrecoverableKeyException {
final KeyStore store = readKeyStore(path, "PKCS12", password);
final Enumeration<String> enumeration = store.aliases();
final Map<Certificate, Key> map = new HashMap<>(store.size());
while (enumeration.hasMoreElements()) {
final String alias = enumeration.nextElement();
if (store.isKeyEntry(alias)) {
final char[] pass = keyPassword.apply(alias);
map.put(store.getCertificate(alias), store.getKey(alias, pass));
}
}
return map;
}
/** /**
* Generates a CA certificate * Generates a CA certificate
*/ */
@ -292,24 +364,25 @@ public class CertUtils {
* Generates a signed certificate using the provided CA private key and information from the CA certificate * Generates a signed certificate using the provided CA private key and information from the CA certificate
*/ */
public static X509Certificate generateSignedCertificate(X500Principal principal, GeneralNames subjectAltNames, KeyPair keyPair, public static X509Certificate generateSignedCertificate(X500Principal principal, GeneralNames subjectAltNames, KeyPair keyPair,
X509Certificate caCert, PrivateKey caPrivKey, int days) X509Certificate caCert, PrivateKey caPrivKey, int days)
throws OperatorCreationException, CertificateException, CertIOException, NoSuchAlgorithmException { throws OperatorCreationException, CertificateException, CertIOException, NoSuchAlgorithmException {
return generateSignedCertificate(principal, subjectAltNames, keyPair, caCert, caPrivKey, false, days); return generateSignedCertificate(principal, subjectAltNames, keyPair, caCert, caPrivKey, false, days);
} }
/** /**
* Generates a signed certificate * Generates a signed certificate
* @param principal the principal of the certificate; commonly referred to as the distinguished name (DN) *
* @param principal the principal of the certificate; commonly referred to as the distinguished name (DN)
* @param subjectAltNames the subject alternative names that should be added to the certificate as an X509v3 extension. May be * @param subjectAltNames the subject alternative names that should be added to the certificate as an X509v3 extension. May be
* {@code null} * {@code null}
* @param keyPair the key pair that will be associated with the certificate * @param keyPair the key pair that will be associated with the certificate
* @param caCert the CA certificate. If {@code null}, this results in a self signed certificate * @param caCert the CA certificate. If {@code null}, this results in a self signed certificate
* @param caPrivKey the CA private key. If {@code null}, this results in a self signed certificate * @param caPrivKey the CA private key. If {@code null}, this results in a self signed certificate
* @param isCa whether or not the generated certificate is a CA * @param isCa whether or not the generated certificate is a CA
* @return a signed {@link X509Certificate} * @return a signed {@link X509Certificate}
*/ */
private static X509Certificate generateSignedCertificate(X500Principal principal, GeneralNames subjectAltNames, KeyPair keyPair, private static X509Certificate generateSignedCertificate(X500Principal principal, GeneralNames subjectAltNames, KeyPair keyPair,
X509Certificate caCert, PrivateKey caPrivKey, boolean isCa, int days) X509Certificate caCert, PrivateKey caPrivKey, boolean isCa, int days)
throws NoSuchAlgorithmException, CertificateException, CertIOException, OperatorCreationException { throws NoSuchAlgorithmException, CertificateException, CertIOException, OperatorCreationException {
final DateTime notBefore = new DateTime(DateTimeZone.UTC); final DateTime notBefore = new DateTime(DateTimeZone.UTC);
if (days < 1) { if (days < 1) {
@ -353,10 +426,11 @@ public class CertUtils {
/** /**
* Generates a certificate signing request * Generates a certificate signing request
* @param keyPair the key pair that will be associated by the certificate generated from the certificate signing request *
* @param keyPair the key pair that will be associated by the certificate generated from the certificate signing request
* @param principal the principal of the certificate; commonly referred to as the distinguished name (DN) * @param principal the principal of the certificate; commonly referred to as the distinguished name (DN)
* @param sanList the subject alternative names that should be added to the certificate as an X509v3 extension. May be * @param sanList the subject alternative names that should be added to the certificate as an X509v3 extension. May be
* {@code null} * {@code null}
* @return a certificate signing request * @return a certificate signing request
*/ */
static PKCS10CertificationRequest generateCSR(KeyPair keyPair, X500Principal principal, GeneralNames sanList) static PKCS10CertificationRequest generateCSR(KeyPair keyPair, X500Principal principal, GeneralNames sanList)

View File

@ -0,0 +1,725 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.ssl;
import javax.security.auth.x500.X500Principal;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.PosixFileAttributeView;
import java.nio.file.attribute.PosixFilePermission;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import joptsimple.ArgumentAcceptingOptionSpec;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import org.bouncycastle.asn1.DERIA5String;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.openssl.PEMEncryptor;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import org.bouncycastle.openssl.jcajce.JcePEMEncryptorBuilder;
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
import org.elasticsearch.ExceptionsHelper;
import org.elasticsearch.cli.EnvironmentAwareCommand;
import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.Terminal;
import org.elasticsearch.cli.UserException;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.SuppressForbidden;
import org.elasticsearch.common.io.PathUtils;
import org.elasticsearch.common.network.InetAddresses;
import org.elasticsearch.common.util.set.Sets;
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
import org.elasticsearch.common.xcontent.ObjectParser;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.env.Environment;
/**
* CLI tool to make generation of certificates or certificate requests easier for users
* @deprecated Replaced by {@link CertificateTool}
*/
@Deprecated
public class CertificateGenerateTool extends EnvironmentAwareCommand {
private static final String AUTO_GEN_CA_DN = "CN=Elastic Certificate Tool Autogenerated CA";
private static final String DESCRIPTION = "Simplifies certificate creation for use with the Elastic Stack";
private static final String DEFAULT_CSR_FILE = "csr-bundle.zip";
private static final String DEFAULT_CERT_FILE = "certificate-bundle.zip";
private static final int DEFAULT_DAYS = 3 * 365;
private static final int FILE_EXTENSION_LENGTH = 4;
static final int MAX_FILENAME_LENGTH = 255 - FILE_EXTENSION_LENGTH;
private static final Pattern ALLOWED_FILENAME_CHAR_PATTERN =
Pattern.compile("[a-zA-Z0-9!@#$%^&{}\\[\\]()_+\\-=,.~'` ]{1," + MAX_FILENAME_LENGTH + "}");
private static final int DEFAULT_KEY_SIZE = 2048;
/**
* Wraps the certgen object parser.
*/
private static class InputFileParser {
private static final ObjectParser<List<CertificateInformation>, Void> PARSER = new ObjectParser<>("certgen");
// if the class initializer here runs before the main method, logging will not have been configured; this will lead to status logger
// error messages from the class initializer for ParseField since it creates Logger instances; therefore, we bury the initialization
// of the parser in this class so that we can defer initialization until after logging has been initialized
static {
@SuppressWarnings("unchecked") final ConstructingObjectParser<CertificateInformation, Void> instanceParser =
new ConstructingObjectParser<>(
"instances",
a -> new CertificateInformation(
(String) a[0], (String) (a[1] == null ? a[0] : a[1]),
(List<String>) a[2], (List<String>) a[3], (List<String>) a[4]));
instanceParser.declareString(ConstructingObjectParser.constructorArg(), new ParseField("name"));
instanceParser.declareString(ConstructingObjectParser.optionalConstructorArg(), new ParseField("filename"));
instanceParser.declareStringArray(ConstructingObjectParser.optionalConstructorArg(), new ParseField("ip"));
instanceParser.declareStringArray(ConstructingObjectParser.optionalConstructorArg(), new ParseField("dns"));
instanceParser.declareStringArray(ConstructingObjectParser.optionalConstructorArg(), new ParseField("cn"));
PARSER.declareObjectArray(List::addAll, instanceParser, new ParseField("instances"));
}
}
private final OptionSpec<String> outputPathSpec;
private final OptionSpec<Void> csrSpec;
private final OptionSpec<String> caCertPathSpec;
private final OptionSpec<String> caKeyPathSpec;
private final OptionSpec<String> caPasswordSpec;
private final OptionSpec<String> caDnSpec;
private final OptionSpec<Integer> keysizeSpec;
private final OptionSpec<String> inputFileSpec;
private final OptionSpec<Integer> daysSpec;
private final ArgumentAcceptingOptionSpec<String> p12Spec;
CertificateGenerateTool() {
super(DESCRIPTION);
outputPathSpec = parser.accepts("out", "path of the zip file that the output should be written to")
.withRequiredArg();
csrSpec = parser.accepts("csr", "only generate certificate signing requests");
caCertPathSpec = parser.accepts("cert", "path to an existing ca certificate").availableUnless(csrSpec).withRequiredArg();
caKeyPathSpec = parser.accepts("key", "path to an existing ca private key")
.availableIf(caCertPathSpec)
.requiredIf(caCertPathSpec)
.withRequiredArg();
caPasswordSpec = parser.accepts("pass", "password for an existing ca private key or the generated ca private key")
.availableUnless(csrSpec)
.withOptionalArg();
caDnSpec = parser.accepts("dn", "distinguished name to use for the generated ca. defaults to " + AUTO_GEN_CA_DN)
.availableUnless(caCertPathSpec)
.availableUnless(csrSpec)
.withRequiredArg();
keysizeSpec = parser.accepts("keysize", "size in bits of RSA keys").withRequiredArg().ofType(Integer.class);
inputFileSpec = parser.accepts("in", "file containing details of the instances in yaml format").withRequiredArg();
daysSpec = parser.accepts("days", "number of days that the generated certificates are valid")
.availableUnless(csrSpec)
.withRequiredArg()
.ofType(Integer.class);
p12Spec = parser.accepts("p12", "output a p12 (PKCS#12) version for each certificate/key pair, with optional password")
.availableUnless(csrSpec)
.withOptionalArg();
}
public static void main(String[] args) throws Exception {
new CertificateGenerateTool().main(args, Terminal.DEFAULT);
}
@Override
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
final boolean csrOnly = options.has(csrSpec);
printIntro(terminal, csrOnly);
final Path outputFile = getOutputFile(terminal, outputPathSpec.value(options), csrOnly ? DEFAULT_CSR_FILE : DEFAULT_CERT_FILE);
final String inputFile = inputFileSpec.value(options);
final int keysize = options.has(keysizeSpec) ? keysizeSpec.value(options) : DEFAULT_KEY_SIZE;
if (csrOnly) {
Collection<CertificateInformation> certificateInformations = getCertificateInformationList(terminal, inputFile);
generateAndWriteCsrs(outputFile, certificateInformations, keysize);
} else {
final String dn = options.has(caDnSpec) ? caDnSpec.value(options) : AUTO_GEN_CA_DN;
final boolean prompt = options.has(caPasswordSpec);
final char[] keyPass = options.hasArgument(caPasswordSpec) ? caPasswordSpec.value(options).toCharArray() : null;
final int days = options.hasArgument(daysSpec) ? daysSpec.value(options) : DEFAULT_DAYS;
final char[] p12Password;
if (options.hasArgument(p12Spec)) {
p12Password = p12Spec.value(options).toCharArray();
} else if (options.has(p12Spec)) {
p12Password = new char[0];
} else {
p12Password = null;
}
CAInfo caInfo = getCAInfo(terminal, dn, caCertPathSpec.value(options), caKeyPathSpec.value(options), keyPass, prompt, env,
keysize, days);
Collection<CertificateInformation> certificateInformations = getCertificateInformationList(terminal, inputFile);
generateAndWriteSignedCertificates(outputFile, certificateInformations, caInfo, keysize, days, p12Password);
}
printConclusion(terminal, csrOnly, outputFile);
}
@Override
protected void printAdditionalHelp(Terminal terminal) {
terminal.println("Simplifies the generation of certificate signing requests and signed");
terminal.println("certificates. The tool runs interactively unless the 'in' and 'out' parameters");
terminal.println("are specified. In the interactive mode, the tool will prompt for required");
terminal.println("values that have not been provided through the use of command line options.");
terminal.println("");
}
/**
* Checks for output file in the user specified options or prompts the user for the output file
*
* @param terminal terminal to communicate with a user
* @param outputPath user specified output file, may be {@code null}
* @return a {@link Path} to the output file
*/
static Path getOutputFile(Terminal terminal, String outputPath, String defaultFilename) throws IOException {
Path file;
if (outputPath != null) {
file = resolvePath(outputPath);
} else {
file = resolvePath(defaultFilename);
String input = terminal.readText("Please enter the desired output file [" + file + "]: ");
if (input.isEmpty() == false) {
file = resolvePath(input);
}
}
return file.toAbsolutePath();
}
@SuppressForbidden(reason = "resolve paths against CWD for a CLI tool")
private static Path resolvePath(String pathStr) {
return PathUtils.get(pathStr).normalize();
}
/**
* This method handles the collection of information about each instance that is necessary to generate a certificate. The user may
* be prompted or the information can be gathered from a file
* @param terminal the terminal to use for user interaction
* @param inputFile an optional file that will be used to load the instance information
* @return a {@link Collection} of {@link CertificateInformation} that represents each instance
*/
static Collection<CertificateInformation> getCertificateInformationList(Terminal terminal, String inputFile)
throws Exception {
if (inputFile != null) {
return parseAndValidateFile(terminal, resolvePath(inputFile).toAbsolutePath());
}
Map<String, CertificateInformation> map = new HashMap<>();
boolean done = false;
while (done == false) {
String name = terminal.readText("Enter instance name: ");
if (name.isEmpty() == false) {
final boolean isNameValidFilename = Name.isValidFilename(name);
String filename = terminal.readText("Enter name for directories and files " + (isNameValidFilename ? "[" + name + "]" : "")
+ ": " );
if (filename.isEmpty() && isNameValidFilename) {
filename = name;
}
String ipAddresses = terminal.readText("Enter IP Addresses for instance (comma-separated if more than one) []: ");
String dnsNames = terminal.readText("Enter DNS names for instance (comma-separated if more than one) []: ");
List<String> ipList = Arrays.asList(Strings.splitStringByCommaToArray(ipAddresses));
List<String> dnsList = Arrays.asList(Strings.splitStringByCommaToArray(dnsNames));
List<String> commonNames = null;
CertificateInformation information = new CertificateInformation(name, filename, ipList, dnsList, commonNames);
List<String> validationErrors = information.validate();
if (validationErrors.isEmpty()) {
if (map.containsKey(name)) {
terminal.println("Overwriting previously defined instance information [" + name + "]");
}
map.put(name, information);
} else {
for (String validationError : validationErrors) {
terminal.println(validationError);
}
terminal.println("Skipping entry as invalid values were found");
}
} else {
terminal.println("A name must be provided");
}
String exit = terminal.readText("Would you like to specify another instance? Press 'y' to continue entering instance " +
"information: ");
if ("y".equals(exit) == false) {
done = true;
}
}
return map.values();
}
static Collection<CertificateInformation> parseAndValidateFile(Terminal terminal, Path file) throws Exception {
final Collection<CertificateInformation> config = parseFile(file);
boolean hasError = false;
for (CertificateInformation certInfo : config) {
final List<String> errors = certInfo.validate();
if (errors.size() > 0) {
hasError = true;
terminal.println(Terminal.Verbosity.SILENT, "Configuration for instance " + certInfo.name.originalName
+ " has invalid details");
for (String message : errors) {
terminal.println(Terminal.Verbosity.SILENT, " * " + message);
}
terminal.println("");
}
}
if (hasError) {
throw new UserException(ExitCodes.CONFIG, "File " + file + " contains invalid configuration details (see messages above)");
}
return config;
}
/**
* Parses the input file to retrieve the certificate information
* @param file the file to parse
* @return a collection of certificate information
*/
static Collection<CertificateInformation> parseFile(Path file) throws Exception {
try (Reader reader = Files.newBufferedReader(file)) {
// EMPTY is safe here because we never use namedObject
XContentParser xContentParser = XContentType.YAML.xContent().createParser(NamedXContentRegistry.EMPTY, reader);
return InputFileParser.PARSER.parse(xContentParser, new ArrayList<>(), null);
}
}
/**
* Generates certificate signing requests and writes them out to the specified file in zip format
* @param outputFile the file to write the output to. This file must not already exist
* @param certInfo the details to use in the certificate signing requests
*/
static void generateAndWriteCsrs(Path outputFile, Collection<CertificateInformation> certInfo, int keysize) throws Exception {
fullyWriteFile(outputFile, (outputStream, pemWriter) -> {
for (CertificateInformation certificateInformation : certInfo) {
KeyPair keyPair = CertUtils.generateKeyPair(keysize);
GeneralNames sanList = getSubjectAlternativeNamesValue(certificateInformation.ipAddresses, certificateInformation.dnsNames,
certificateInformation.commonNames);
PKCS10CertificationRequest csr = CertUtils.generateCSR(keyPair, certificateInformation.name.x500Principal, sanList);
final String dirName = certificateInformation.name.filename + "/";
ZipEntry zipEntry = new ZipEntry(dirName);
assert zipEntry.isDirectory();
outputStream.putNextEntry(zipEntry);
// write csr
outputStream.putNextEntry(new ZipEntry(dirName + certificateInformation.name.filename + ".csr"));
pemWriter.writeObject(csr);
pemWriter.flush();
outputStream.closeEntry();
// write private key
outputStream.putNextEntry(new ZipEntry(dirName + certificateInformation.name.filename + ".key"));
pemWriter.writeObject(keyPair.getPrivate());
pemWriter.flush();
outputStream.closeEntry();
}
});
}
/**
* Returns the CA certificate and private key that will be used to sign certificates. These may be specified by the user or
* automatically generated
*
* @param terminal the terminal to use for prompting the user
* @param dn the distinguished name to use for the CA
* @param caCertPath the path to the CA certificate or {@code null} if not provided
* @param caKeyPath the path to the CA private key or {@code null} if not provided
* @param prompt whether we should prompt the user for a password
* @param keyPass the password to the private key. If not present and the key is encrypted the user will be prompted
* @param env the environment for this tool to resolve files with
* @param keysize the size of the key in bits
* @param days the number of days that the certificate should be valid for
* @return CA cert and private key
*/
static CAInfo getCAInfo(Terminal terminal, String dn, String caCertPath, String caKeyPath, char[] keyPass, boolean prompt,
Environment env, int keysize, int days) throws Exception {
if (caCertPath != null) {
assert caKeyPath != null;
final String resolvedCaCertPath = resolvePath(caCertPath).toAbsolutePath().toString();
Certificate[] certificates = CertUtils.readCertificates(Collections.singletonList(resolvedCaCertPath), env);
if (certificates.length != 1) {
throw new IllegalArgumentException("expected a single certificate in file [" + caCertPath + "] but found [" +
certificates.length + "]");
}
Certificate caCert = certificates[0];
PrivateKey privateKey = readPrivateKey(caKeyPath, keyPass, terminal, prompt);
return new CAInfo((X509Certificate) caCert, privateKey);
}
// generate the CA keys and cert
X500Principal x500Principal = new X500Principal(dn);
KeyPair keyPair = CertUtils.generateKeyPair(keysize);
Certificate caCert = CertUtils.generateCACertificate(x500Principal, keyPair, days);
final char[] password;
if (prompt) {
password = terminal.readSecret("Enter password for CA private key: ");
} else {
password = keyPass;
}
return new CAInfo((X509Certificate) caCert, keyPair.getPrivate(), true, password);
}
/**
* Generates signed certificates in PEM format stored in a zip file
* @param outputFile the file that the certificates will be written to. This file must not exist
* @param certificateInformations details for creation of the certificates
* @param caInfo the CA information to sign the certificates with
* @param keysize the size of the key in bits
* @param days the number of days that the certificate should be valid for
*/
static void generateAndWriteSignedCertificates(Path outputFile, Collection<CertificateInformation> certificateInformations,
CAInfo caInfo, int keysize, int days, char[] pkcs12Password) throws Exception {
fullyWriteFile(outputFile, (outputStream, pemWriter) -> {
// write out the CA info first if it was generated
writeCAInfoIfGenerated(outputStream, pemWriter, caInfo);
for (CertificateInformation certificateInformation : certificateInformations) {
KeyPair keyPair = CertUtils.generateKeyPair(keysize);
Certificate certificate = CertUtils.generateSignedCertificate(certificateInformation.name.x500Principal,
getSubjectAlternativeNamesValue(certificateInformation.ipAddresses, certificateInformation.dnsNames,
certificateInformation.commonNames),
keyPair, caInfo.caCert, caInfo.privateKey, days);
final String dirName = certificateInformation.name.filename + "/";
ZipEntry zipEntry = new ZipEntry(dirName);
assert zipEntry.isDirectory();
outputStream.putNextEntry(zipEntry);
// write cert
final String entryBase = dirName + certificateInformation.name.filename;
outputStream.putNextEntry(new ZipEntry(entryBase + ".crt"));
pemWriter.writeObject(certificate);
pemWriter.flush();
outputStream.closeEntry();
// write private key
outputStream.putNextEntry(new ZipEntry(entryBase + ".key"));
pemWriter.writeObject(keyPair.getPrivate());
pemWriter.flush();
outputStream.closeEntry();
if (pkcs12Password != null) {
final KeyStore pkcs12 = KeyStore.getInstance("PKCS12");
pkcs12.load(null);
pkcs12.setKeyEntry(certificateInformation.name.originalName, keyPair.getPrivate(), pkcs12Password,
new Certificate[]{certificate});
outputStream.putNextEntry(new ZipEntry(entryBase + ".p12"));
pkcs12.store(outputStream, pkcs12Password);
outputStream.closeEntry();
}
}
});
}
/**
* This method handles the deletion of a file in the case of a partial write
* @param file the file that is being written to
* @param writer writes the contents of the file
*/
private static void fullyWriteFile(Path file, Writer writer) throws Exception {
boolean success = false;
try (OutputStream outputStream = Files.newOutputStream(file, StandardOpenOption.CREATE_NEW);
ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream, StandardCharsets.UTF_8);
JcaPEMWriter pemWriter = new JcaPEMWriter(new OutputStreamWriter(zipOutputStream, StandardCharsets.UTF_8))) {
writer.write(zipOutputStream, pemWriter);
// set permissions to 600
PosixFileAttributeView view = Files.getFileAttributeView(file, PosixFileAttributeView.class);
if (view != null) {
view.setPermissions(Sets.newHashSet(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE));
}
success = true;
} finally {
if (success == false) {
Files.deleteIfExists(file);
}
}
}
/**
* This method handles writing out the certificate authority cert and private key if the certificate authority was generated by
* this invocation of the tool
* @param outputStream the output stream to write to
* @param pemWriter the writer for PEM objects
* @param info the certificate authority information
*/
private static void writeCAInfoIfGenerated(ZipOutputStream outputStream, JcaPEMWriter pemWriter, CAInfo info) throws Exception {
if (info.generated) {
final String caDirName = "ca/";
ZipEntry zipEntry = new ZipEntry(caDirName);
assert zipEntry.isDirectory();
outputStream.putNextEntry(zipEntry);
outputStream.putNextEntry(new ZipEntry(caDirName + "ca.crt"));
pemWriter.writeObject(info.caCert);
pemWriter.flush();
outputStream.closeEntry();
outputStream.putNextEntry(new ZipEntry(caDirName + "ca.key"));
if (info.password != null && info.password.length > 0) {
try {
PEMEncryptor encryptor = new JcePEMEncryptorBuilder("DES-EDE3-CBC").setProvider(CertUtils.BC_PROV).build(info.password);
pemWriter.writeObject(info.privateKey, encryptor);
} finally {
// we can safely nuke the password chars now
Arrays.fill(info.password, (char) 0);
}
} else {
pemWriter.writeObject(info.privateKey);
}
pemWriter.flush();
outputStream.closeEntry();
}
}
private static void printIntro(Terminal terminal, boolean csr) {
terminal.println("******************************************************************************");
terminal.println("Note: The 'certgen' tool has been deprecated in favour of the 'certutil' tool.");
terminal.println(" This command will be removed in a future release of X-Pack.");
terminal.println("******************************************************************************");
terminal.println("");
terminal.println("This tool assists you in the generation of X.509 certificates and certificate");
terminal.println("signing requests for use with SSL in the Elastic stack. Depending on the command");
terminal.println("line option specified, you may be prompted for the following:");
terminal.println("");
terminal.println("* The path to the output file");
if (csr) {
terminal.println(" * The output file is a zip file containing the certificate signing requests");
terminal.println(" and private keys for each instance.");
} else {
terminal.println(" * The output file is a zip file containing the signed certificates and");
terminal.println(" private keys for each instance. If a Certificate Authority was generated,");
terminal.println(" the certificate and private key will also be included in the output file.");
}
terminal.println("* Information about each instance");
terminal.println(" * An instance is any piece of the Elastic Stack that requires a SSL certificate.");
terminal.println(" Depending on your configuration, Elasticsearch, Logstash, Kibana, and Beats");
terminal.println(" may all require a certificate and private key.");
terminal.println(" * The minimum required value for each instance is a name. This can simply be the");
terminal.println(" hostname, which will be used as the Common Name of the certificate. A full");
terminal.println(" distinguished name may also be used.");
terminal.println(" * A filename value may be required for each instance. This is necessary when the");
terminal.println(" name would result in an invalid file or directory name. The name provided here");
terminal.println(" is used as the directory name (within the zip) and the prefix for the key and");
terminal.println(" certificate files. The filename is required if you are prompted and the name");
terminal.println(" is not displayed in the prompt.");
terminal.println(" * IP addresses and DNS names are optional. Multiple values can be specified as a");
terminal.println(" comma separated string. If no IP addresses or DNS names are provided, you may");
terminal.println(" disable hostname verification in your SSL configuration.");
if (csr == false) {
terminal.println("* Certificate Authority private key password");
terminal.println(" * The password may be left empty if desired.");
}
terminal.println("");
terminal.println("Let's get started...");
terminal.println("");
}
private static void printConclusion(Terminal terminal, boolean csr, Path outputFile) {
if (csr) {
terminal.println("Certificate signing requests written to " + outputFile);
terminal.println("");
terminal.println("This file should be properly secured as it contains the private keys for all");
terminal.println("instances.");
terminal.println("");
terminal.println("After unzipping the file, there will be a directory for each instance containing");
terminal.println("the certificate signing request and the private key. Provide the certificate");
terminal.println("signing requests to your certificate authority. Once you have received the");
terminal.println("signed certificate, copy the signed certificate, key, and CA certificate to the");
terminal.println("configuration directory of the Elastic product that they will be used for and");
terminal.println("follow the SSL configuration instructions in the product guide.");
} else {
terminal.println("Certificates written to " + outputFile);
terminal.println("");
terminal.println("This file should be properly secured as it contains the private keys for all");
terminal.println("instances and the certificate authority.");
terminal.println("");
terminal.println("After unzipping the file, there will be a directory for each instance containing");
terminal.println("the certificate and private key. Copy the certificate, key, and CA certificate");
terminal.println("to the configuration directory of the Elastic product that they will be used for");
terminal.println("and follow the SSL configuration instructions in the product guide.");
terminal.println("");
terminal.println("For client applications, you may only need to copy the CA certificate and");
terminal.println("configure the client to trust this certificate.");
}
}
/**
* Helper method to read a private key and support prompting of user for a key. To avoid passwords being placed as an argument we
* can prompt the user for their password if we encounter an encrypted key.
* @param path the path to the private key
* @param password the password provided by the user or {@code null}
* @param terminal the terminal to use for user interaction
* @param prompt whether to prompt the user or not
* @return the {@link PrivateKey} that was read from the file
*/
private static PrivateKey readPrivateKey(String path, char[] password, Terminal terminal, boolean prompt)
throws Exception {
AtomicReference<char[]> passwordReference = new AtomicReference<>(password);
try (Reader reader = Files.newBufferedReader(resolvePath(path), StandardCharsets.UTF_8)) {
return CertUtils.readPrivateKey(reader, () -> {
if (password != null || prompt == false) {
return password;
}
char[] promptedValue = terminal.readSecret("Enter password for CA private key: ");
passwordReference.set(promptedValue);
return promptedValue;
});
} finally {
if (passwordReference.get() != null) {
Arrays.fill(passwordReference.get(), (char) 0);
}
}
}
private static GeneralNames getSubjectAlternativeNamesValue(List<String> ipAddresses, List<String> dnsNames, List<String> commonNames) {
Set<GeneralName> generalNameList = new HashSet<>();
for (String ip : ipAddresses) {
generalNameList.add(new GeneralName(GeneralName.iPAddress, ip));
}
for (String dns : dnsNames) {
generalNameList.add(new GeneralName(GeneralName.dNSName, dns));
}
for (String cn : commonNames) {
generalNameList.add(CertUtils.createCommonName(cn));
}
if (generalNameList.isEmpty()) {
return null;
}
return new GeneralNames(generalNameList.toArray(new GeneralName[0]));
}
static class CertificateInformation {
final Name name;
final List<String> ipAddresses;
final List<String> dnsNames;
final List<String> commonNames;
CertificateInformation(String name, String filename, List<String> ipAddresses, List<String> dnsNames, List<String> commonNames) {
this.name = Name.fromUserProvidedName(name, filename);
this.ipAddresses = ipAddresses == null ? Collections.emptyList() : ipAddresses;
this.dnsNames = dnsNames == null ? Collections.emptyList() : dnsNames;
this.commonNames = commonNames == null ? Collections.emptyList() : commonNames;
}
List<String> validate() {
List<String> errors = new ArrayList<>();
if (name.error != null) {
errors.add(name.error);
}
for (String ip : ipAddresses) {
if (InetAddresses.isInetAddress(ip) == false) {
errors.add("[" + ip + "] is not a valid IP address");
}
}
for (String dnsName : dnsNames) {
if (DERIA5String.isIA5String(dnsName) == false) {
errors.add("[" + dnsName + "] is not a valid DNS name");
}
}
return errors;
}
}
static class Name {
final String originalName;
final X500Principal x500Principal;
final String filename;
final String error;
private Name(String name, X500Principal x500Principal, String filename, String error) {
this.originalName = name;
this.x500Principal = x500Principal;
this.filename = filename;
this.error = error;
}
static Name fromUserProvidedName(String name, String filename) {
if ("ca".equals(name)) {
return new Name(name, null, null, "[ca] may not be used as an instance name");
}
final X500Principal principal;
try {
if (name.contains("=")) {
principal = new X500Principal(name);
} else {
principal = new X500Principal("CN=" + name);
}
} catch (IllegalArgumentException e) {
String error = "[" + name + "] could not be converted to a valid DN\n" + e.getMessage() + "\n"
+ ExceptionsHelper.stackTrace(e);
return new Name(name, null, null, error);
}
boolean validFilename = isValidFilename(filename);
if (validFilename == false) {
return new Name(name, principal, null, "[" + filename + "] is not a valid filename");
}
return new Name(name, principal, resolvePath(filename).toString(), null);
}
static boolean isValidFilename(String name) {
return ALLOWED_FILENAME_CHAR_PATTERN.matcher(name).matches()
&& ALLOWED_FILENAME_CHAR_PATTERN.matcher(resolvePath(name).toString()).matches()
&& name.startsWith(".") == false;
}
@Override
public String toString() {
return getClass().getSimpleName()
+ "{original=[" + originalName + "] principal=[" + x500Principal
+ "] file=[" + filename + "] err=[" + error + "]}";
}
}
static class CAInfo {
final X509Certificate caCert;
final PrivateKey privateKey;
final boolean generated;
final char[] password;
CAInfo(X509Certificate caCert, PrivateKey privateKey) {
this(caCert, privateKey, false, null);
}
CAInfo(X509Certificate caCert, PrivateKey privateKey, boolean generated, char[] password) {
this.caCert = caCert;
this.privateKey = privateKey;
this.generated = generated;
this.password = password;
}
}
private interface Writer {
void write(ZipOutputStream zipOutputStream, JcaPEMWriter pemWriter) throws Exception;
}
}

View File

@ -16,7 +16,7 @@ import org.hamcrest.Matchers;
public class TestMatchers extends Matchers { public class TestMatchers extends Matchers {
public static Matcher<Path> pathExists(Path path, LinkOption... options) { public static Matcher<Path> pathExists(Path path, LinkOption... options) {
return new CustomMatcher<Path>("Path " + path + " doesn't exist") { return new CustomMatcher<Path>("Path " + path + " exists") {
@Override @Override
public boolean matches(Object item) { public boolean matches(Object item) {
return Files.exists(path, options); return Files.exists(path, options);

View File

@ -0,0 +1,535 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.ssl;
import javax.security.auth.x500.X500Principal;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.net.InetAddress;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
import java.security.Key;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAKey;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import com.google.common.jimfs.Configuration;
import com.google.common.jimfs.Jimfs;
import org.apache.lucene.util.IOUtils;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.ASN1String;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.pkcs.Attribute;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.Extensions;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.openssl.PEMEncryptedKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
import org.elasticsearch.cli.MockTerminal;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.SuppressForbidden;
import org.elasticsearch.common.io.PathUtils;
import org.elasticsearch.common.network.NetworkAddress;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.test.SecuritySettingsSource;
import org.elasticsearch.xpack.ssl.CertificateGenerateTool.CAInfo;
import org.elasticsearch.xpack.ssl.CertificateGenerateTool.CertificateInformation;
import org.elasticsearch.xpack.ssl.CertificateGenerateTool.Name;
import org.hamcrest.Matchers;
import org.junit.After;
import static org.elasticsearch.test.TestMatchers.pathExists;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue;
/**
* Unit tests for the tool used to simplify SSL certificate generation
*/
public class CertificateGenerateToolTests extends ESTestCase {
private FileSystem jimfs;
private Path initTempDir() throws Exception {
Configuration conf = Configuration.unix().toBuilder().setAttributeViews("posix").build();
jimfs = Jimfs.newFileSystem(conf);
Path tempDir = jimfs.getPath("temp");
IOUtils.rm(tempDir);
Files.createDirectories(tempDir);
return tempDir;
}
@After
public void tearDown() throws Exception {
IOUtils.close(jimfs);
super.tearDown();
}
public void testOutputDirectory() throws Exception {
Path outputDir = createTempDir();
Path outputFile = outputDir.resolve("certs.zip");
MockTerminal terminal = new MockTerminal();
// test with a user provided dir
Path resolvedOutputFile = CertificateGenerateTool.getOutputFile(terminal, outputFile.toString(), null);
assertEquals(outputFile, resolvedOutputFile);
assertTrue(terminal.getOutput().isEmpty());
// test without a user provided directory
Path userPromptedOutputFile = outputDir.resolve("csr");
assertFalse(Files.exists(userPromptedOutputFile));
terminal.addTextInput(userPromptedOutputFile.toString());
resolvedOutputFile = CertificateGenerateTool.getOutputFile(terminal, null, "out.zip");
assertEquals(userPromptedOutputFile, resolvedOutputFile);
assertTrue(terminal.getOutput().isEmpty());
// test with empty user input
String defaultFilename = randomAlphaOfLengthBetween(1, 10);
Path expectedDefaultPath = resolvePath(defaultFilename);
terminal.addTextInput("");
resolvedOutputFile = CertificateGenerateTool.getOutputFile(terminal, null, defaultFilename);
assertEquals(expectedDefaultPath, resolvedOutputFile);
assertTrue(terminal.getOutput().isEmpty());
}
public void testPromptingForInstanceInformation() throws Exception {
final int numberOfInstances = scaledRandomIntBetween(1, 12);
Map<String, Map<String, String>> instanceInput = new HashMap<>(numberOfInstances);
for (int i = 0; i < numberOfInstances; i++) {
final String name;
while (true) {
String randomName = getValidRandomInstanceName();
if (instanceInput.containsKey(randomName) == false) {
name = randomName;
break;
}
}
Map<String, String> instanceInfo = new HashMap<>();
instanceInput.put(name, instanceInfo);
instanceInfo.put("ip", randomFrom("127.0.0.1", "::1", "192.168.1.1,::1", ""));
instanceInfo.put("dns", randomFrom("localhost", "localhost.localdomain", "localhost,myhost", ""));
logger.info("instance [{}] name [{}] [{}]", i, name, instanceInfo);
}
int count = 0;
MockTerminal terminal = new MockTerminal();
for (Entry<String, Map<String, String>> entry : instanceInput.entrySet()) {
terminal.addTextInput(entry.getKey());
terminal.addTextInput("");
terminal.addTextInput(entry.getValue().get("ip"));
terminal.addTextInput(entry.getValue().get("dns"));
count++;
if (count == numberOfInstances) {
terminal.addTextInput("n");
} else {
terminal.addTextInput("y");
}
}
Collection<CertificateInformation> certInfos = CertificateGenerateTool.getCertificateInformationList(terminal, null);
logger.info("certificate tool output:\n{}", terminal.getOutput());
assertEquals(numberOfInstances, certInfos.size());
for (CertificateInformation certInfo : certInfos) {
String name = certInfo.name.originalName;
Map<String, String> instanceInfo = instanceInput.get(name);
assertNotNull("did not find map for " + name, instanceInfo);
List<String> expectedIps = Arrays.asList(Strings.commaDelimitedListToStringArray(instanceInfo.get("ip")));
List<String> expectedDns = Arrays.asList(Strings.commaDelimitedListToStringArray(instanceInfo.get("dns")));
assertEquals(expectedIps, certInfo.ipAddresses);
assertEquals(expectedDns, certInfo.dnsNames);
instanceInput.remove(name);
}
assertEquals(0, instanceInput.size());
final String output = terminal.getOutput();
assertTrue("Output: " + output, output.isEmpty());
}
public void testParsingFile() throws Exception {
Path tempDir = initTempDir();
Path instanceFile = writeInstancesTo(tempDir.resolve("instances.yml"));
Collection<CertificateInformation> certInfos = CertificateGenerateTool.parseFile(instanceFile);
assertEquals(4, certInfos.size());
Map<String, CertificateInformation> certInfosMap =
certInfos.stream().collect(Collectors.toMap((c) -> c.name.originalName, Function.identity()));
CertificateInformation certInfo = certInfosMap.get("node1");
assertEquals(Collections.singletonList("127.0.0.1"), certInfo.ipAddresses);
assertEquals(Collections.singletonList("localhost"), certInfo.dnsNames);
assertEquals(Collections.emptyList(), certInfo.commonNames);
assertEquals("node1", certInfo.name.filename);
certInfo = certInfosMap.get("node2");
assertEquals(Collections.singletonList("::1"), certInfo.ipAddresses);
assertEquals(Collections.emptyList(), certInfo.dnsNames);
assertEquals(Collections.singletonList("node2.elasticsearch"), certInfo.commonNames);
assertEquals("node2", certInfo.name.filename);
certInfo = certInfosMap.get("node3");
assertEquals(Collections.emptyList(), certInfo.ipAddresses);
assertEquals(Collections.emptyList(), certInfo.dnsNames);
assertEquals(Collections.emptyList(), certInfo.commonNames);
assertEquals("node3", certInfo.name.filename);
certInfo = certInfosMap.get("CN=different value");
assertEquals(Collections.emptyList(), certInfo.ipAddresses);
assertEquals(Collections.singletonList("node4.mydomain.com"), certInfo.dnsNames);
assertEquals(Collections.emptyList(), certInfo.commonNames);
assertEquals("different file", certInfo.name.filename);
}
public void testGeneratingCsr() throws Exception {
Path tempDir = initTempDir();
Path outputFile = tempDir.resolve("out.zip");
Path instanceFile = writeInstancesTo(tempDir.resolve("instances.yml"));
Collection<CertificateInformation> certInfos = CertificateGenerateTool.parseFile(instanceFile);
assertEquals(4, certInfos.size());
assertFalse(Files.exists(outputFile));
CertificateGenerateTool.generateAndWriteCsrs(outputFile, certInfos, randomFrom(1024, 2048));
assertTrue(Files.exists(outputFile));
Set<PosixFilePermission> perms = Files.getPosixFilePermissions(outputFile);
assertTrue(perms.toString(), perms.contains(PosixFilePermission.OWNER_READ));
assertTrue(perms.toString(), perms.contains(PosixFilePermission.OWNER_WRITE));
assertEquals(perms.toString(), 2, perms.size());
FileSystem fileSystem = FileSystems.newFileSystem(new URI("jar:" + outputFile.toUri()), Collections.emptyMap());
Path zipRoot = fileSystem.getPath("/");
assertFalse(Files.exists(zipRoot.resolve("ca")));
for (CertificateInformation certInfo : certInfos) {
String filename = certInfo.name.filename;
assertTrue(Files.exists(zipRoot.resolve(filename)));
final Path csr = zipRoot.resolve(filename + "/" + filename + ".csr");
assertTrue(Files.exists(csr));
assertTrue(Files.exists(zipRoot.resolve(filename + "/" + filename + ".key")));
PKCS10CertificationRequest request = readCertificateRequest(csr);
assertEquals(certInfo.name.x500Principal.getName(), request.getSubject().toString());
Attribute[] extensionsReq = request.getAttributes(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest);
if (certInfo.ipAddresses.size() > 0 || certInfo.dnsNames.size() > 0) {
assertEquals(1, extensionsReq.length);
Extensions extensions = Extensions.getInstance(extensionsReq[0].getAttributeValues()[0]);
GeneralNames subjAltNames = GeneralNames.fromExtensions(extensions, Extension.subjectAlternativeName);
assertSubjAltNames(subjAltNames, certInfo);
} else {
assertEquals(0, extensionsReq.length);
}
}
}
public void testGeneratingSignedCertificates() throws Exception {
Path tempDir = initTempDir();
Path outputFile = tempDir.resolve("out.zip");
Path instanceFile = writeInstancesTo(tempDir.resolve("instances.yml"));
Collection<CertificateInformation> certInfos = CertificateGenerateTool.parseFile(instanceFile);
assertEquals(4, certInfos.size());
final int keysize = randomFrom(1024, 2048);
final int days = randomIntBetween(1, 1024);
KeyPair keyPair = CertUtils.generateKeyPair(keysize);
X509Certificate caCert = CertUtils.generateCACertificate(new X500Principal("CN=test ca"), keyPair, days);
final boolean generatedCa = randomBoolean();
final char[] keyPassword = randomBoolean() ? SecuritySettingsSource.TEST_PASSWORD.toCharArray() : null;
final char[] pkcs12Password = randomBoolean() ? randomAlphaOfLengthBetween(1, 12).toCharArray() : null;
assertFalse(Files.exists(outputFile));
CAInfo caInfo = new CAInfo(caCert, keyPair.getPrivate(), generatedCa, keyPassword);
CertificateGenerateTool.generateAndWriteSignedCertificates(outputFile, certInfos, caInfo, keysize, days, pkcs12Password);
assertTrue(Files.exists(outputFile));
Set<PosixFilePermission> perms = Files.getPosixFilePermissions(outputFile);
assertTrue(perms.toString(), perms.contains(PosixFilePermission.OWNER_READ));
assertTrue(perms.toString(), perms.contains(PosixFilePermission.OWNER_WRITE));
assertEquals(perms.toString(), 2, perms.size());
FileSystem fileSystem = FileSystems.newFileSystem(new URI("jar:" + outputFile.toUri()), Collections.emptyMap());
Path zipRoot = fileSystem.getPath("/");
if (generatedCa) {
assertTrue(Files.exists(zipRoot.resolve("ca")));
assertTrue(Files.exists(zipRoot.resolve("ca").resolve("ca.crt")));
assertTrue(Files.exists(zipRoot.resolve("ca").resolve("ca.key")));
// check the CA cert
try (Reader reader = Files.newBufferedReader(zipRoot.resolve("ca").resolve("ca.crt"))) {
X509Certificate parsedCaCert = readX509Certificate(reader);
assertThat(parsedCaCert.getSubjectX500Principal().getName(), containsString("test ca"));
assertEquals(caCert, parsedCaCert);
long daysBetween = ChronoUnit.DAYS.between(caCert.getNotBefore().toInstant(), caCert.getNotAfter().toInstant());
assertEquals(days, (int) daysBetween);
}
// check the CA key
if (keyPassword != null) {
try (Reader reader = Files.newBufferedReader(zipRoot.resolve("ca").resolve("ca.key"))) {
PEMParser pemParser = new PEMParser(reader);
Object parsed = pemParser.readObject();
assertThat(parsed, instanceOf(PEMEncryptedKeyPair.class));
char[] zeroChars = new char[keyPassword.length];
Arrays.fill(zeroChars, (char) 0);
assertArrayEquals(zeroChars, keyPassword);
}
}
try (Reader reader = Files.newBufferedReader(zipRoot.resolve("ca").resolve("ca.key"))) {
PrivateKey privateKey = CertUtils.readPrivateKey(reader, () -> keyPassword != null ?
SecuritySettingsSource.TEST_PASSWORD.toCharArray() : null);
assertEquals(caInfo.privateKey, privateKey);
}
} else {
assertFalse(Files.exists(zipRoot.resolve("ca")));
}
for (CertificateInformation certInfo : certInfos) {
String filename = certInfo.name.filename;
assertTrue(Files.exists(zipRoot.resolve(filename)));
final Path cert = zipRoot.resolve(filename + "/" + filename + ".crt");
assertTrue(Files.exists(cert));
assertTrue(Files.exists(zipRoot.resolve(filename + "/" + filename + ".key")));
final Path p12 = zipRoot.resolve(filename + "/" + filename + ".p12");
try (Reader reader = Files.newBufferedReader(cert)) {
X509Certificate certificate = readX509Certificate(reader);
assertEquals(certInfo.name.x500Principal.toString(), certificate.getSubjectX500Principal().getName());
final int sanCount = certInfo.ipAddresses.size() + certInfo.dnsNames.size() + certInfo.commonNames.size();
if (sanCount == 0) {
assertNull(certificate.getSubjectAlternativeNames());
} else {
X509CertificateHolder x509CertHolder = new X509CertificateHolder(certificate.getEncoded());
GeneralNames subjAltNames =
GeneralNames.fromExtensions(x509CertHolder.getExtensions(), Extension.subjectAlternativeName);
assertSubjAltNames(subjAltNames, certInfo);
}
if (pkcs12Password != null) {
assertThat(p12, pathExists(p12));
try (InputStream in = Files.newInputStream(p12)) {
final KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(in, pkcs12Password);
final Certificate p12Certificate = ks.getCertificate(certInfo.name.originalName);
assertThat("Certificate " + certInfo.name, p12Certificate, notNullValue());
assertThat(p12Certificate, equalTo(certificate));
final Key key = ks.getKey(certInfo.name.originalName, pkcs12Password);
assertThat(key, notNullValue());
}
} else {
assertThat(p12, not(pathExists(p12)));
}
}
}
}
public void testGetCAInfo() throws Exception {
Environment env = new Environment(Settings.builder().put("path.home", createTempDir()).build());
Path testNodeCertPath = getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.crt");
Path testNodeKeyPath = getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.pem");
final boolean passwordPrompt = randomBoolean();
MockTerminal terminal = new MockTerminal();
if (passwordPrompt) {
terminal.addSecretInput("testnode");
}
final int days = randomIntBetween(1, 1024);
CAInfo caInfo = CertificateGenerateTool.getCAInfo(terminal, "CN=foo", testNodeCertPath.toString(), testNodeKeyPath.toString(),
passwordPrompt ? null : "testnode".toCharArray(), passwordPrompt, env, randomFrom(1024, 2048), days);
assertTrue(terminal.getOutput().isEmpty());
assertEquals(caInfo.caCert.getSubjectX500Principal().getName(),
"CN=Elasticsearch Test Node,OU=elasticsearch,O=org");
assertThat(caInfo.privateKey.getAlgorithm(), containsString("RSA"));
assertEquals(2048, ((RSAKey) caInfo.privateKey).getModulus().bitLength());
assertFalse(caInfo.generated);
long daysBetween = ChronoUnit.DAYS.between(caInfo.caCert.getNotBefore().toInstant(), caInfo.caCert.getNotAfter().toInstant());
assertEquals(1460L, daysBetween);
// test generation
final boolean passwordProtected = randomBoolean();
final char[] password;
if (passwordPrompt && passwordProtected) {
password = null;
terminal.addSecretInput("testnode");
} else {
password = "testnode".toCharArray();
}
final int keysize = randomFrom(1024, 2048);
caInfo = CertificateGenerateTool.getCAInfo(terminal, "CN=foo bar", null, null, password, passwordProtected && passwordPrompt, env,
keysize, days);
assertTrue(terminal.getOutput().isEmpty());
assertThat(caInfo.caCert, instanceOf(X509Certificate.class));
assertEquals(caInfo.caCert.getSubjectX500Principal().getName(), "CN=foo bar");
assertThat(caInfo.privateKey.getAlgorithm(), containsString("RSA"));
assertTrue(caInfo.generated);
assertEquals(keysize, ((RSAKey) caInfo.privateKey).getModulus().bitLength());
daysBetween = ChronoUnit.DAYS.between(caInfo.caCert.getNotBefore().toInstant(), caInfo.caCert.getNotAfter().toInstant());
assertEquals(days, (int) daysBetween);
}
public void testNameValues() throws Exception {
// good name
Name name = Name.fromUserProvidedName("my instance", "my instance");
assertEquals("my instance", name.originalName);
assertNull(name.error);
assertEquals("CN=my instance", name.x500Principal.getName());
assertEquals("my instance", name.filename);
// too long
String userProvidedName = randomAlphaOfLength(CertificateGenerateTool.MAX_FILENAME_LENGTH + 1);
name = Name.fromUserProvidedName(userProvidedName, userProvidedName);
assertEquals(userProvidedName, name.originalName);
assertThat(name.error, containsString("valid filename"));
// too short
name = Name.fromUserProvidedName("", "");
assertEquals("", name.originalName);
assertThat(name.error, containsString("valid filename"));
assertEquals("CN=", name.x500Principal.getName());
assertNull(name.filename);
// invalid characters only
userProvidedName = "<>|<>*|?\"\\";
name = Name.fromUserProvidedName(userProvidedName, userProvidedName);
assertEquals(userProvidedName, name.originalName);
assertThat(name.error, containsString("valid DN"));
assertNull(name.x500Principal);
assertNull(name.filename);
// invalid for file but DN ok
userProvidedName = "*";
name = Name.fromUserProvidedName(userProvidedName, userProvidedName);
assertEquals(userProvidedName, name.originalName);
assertThat(name.error, containsString("valid filename"));
assertEquals("CN=" + userProvidedName, name.x500Principal.getName());
assertNull(name.filename);
// invalid with valid chars for filename
userProvidedName = "*.mydomain.com";
name = Name.fromUserProvidedName(userProvidedName, userProvidedName);
assertEquals(userProvidedName, name.originalName);
assertThat(name.error, containsString("valid filename"));
assertEquals("CN=" + userProvidedName, name.x500Principal.getName());
// valid but could create hidden file/dir so it is not allowed
userProvidedName = ".mydomain.com";
name = Name.fromUserProvidedName(userProvidedName, userProvidedName);
assertEquals(userProvidedName, name.originalName);
assertThat(name.error, containsString("valid filename"));
assertEquals("CN=" + userProvidedName, name.x500Principal.getName());
}
private PKCS10CertificationRequest readCertificateRequest(Path path) throws Exception {
try (Reader reader = Files.newBufferedReader(path);
PEMParser pemParser = new PEMParser(reader)) {
Object object = pemParser.readObject();
assertThat(object, instanceOf(PKCS10CertificationRequest.class));
return (PKCS10CertificationRequest) object;
}
}
private X509Certificate readX509Certificate(Reader reader) throws Exception {
List<Certificate> list = new ArrayList<>(1);
CertUtils.readCertificates(reader, list, CertificateFactory.getInstance("X.509"));
assertEquals(1, list.size());
assertThat(list.get(0), instanceOf(X509Certificate.class));
return (X509Certificate) list.get(0);
}
private void assertSubjAltNames(GeneralNames subjAltNames, CertificateInformation certInfo) throws Exception {
final int expectedCount = certInfo.ipAddresses.size() + certInfo.dnsNames.size() + certInfo.commonNames.size();
assertEquals(expectedCount, subjAltNames.getNames().length);
Collections.sort(certInfo.dnsNames);
Collections.sort(certInfo.ipAddresses);
for (GeneralName generalName : subjAltNames.getNames()) {
if (generalName.getTagNo() == GeneralName.dNSName) {
String dns = ((ASN1String) generalName.getName()).getString();
assertTrue(certInfo.dnsNames.stream().anyMatch(dns::equals));
} else if (generalName.getTagNo() == GeneralName.iPAddress) {
byte[] ipBytes = DEROctetString.getInstance(generalName.getName()).getOctets();
String ip = NetworkAddress.format(InetAddress.getByAddress(ipBytes));
assertTrue(certInfo.ipAddresses.stream().anyMatch(ip::equals));
} else if (generalName.getTagNo() == GeneralName.otherName) {
ASN1Sequence seq = ASN1Sequence.getInstance(generalName.getName());
assertThat(seq.size(), equalTo(2));
assertThat(seq.getObjectAt(0), instanceOf(ASN1ObjectIdentifier.class));
assertThat(seq.getObjectAt(0).toString(), equalTo(CertUtils.CN_OID));
assertThat(seq.getObjectAt(1), instanceOf(ASN1String.class));
assertThat(seq.getObjectAt(1).toString(), Matchers.isIn(certInfo.commonNames));
} else {
fail("unknown general name with tag " + generalName.getTagNo());
}
}
}
/**
* Gets a random name that is valid for certificate generation. There are some cases where the random value could match one of the
* reserved names like ca, so this method allows us to avoid these issues.
*/
private String getValidRandomInstanceName() {
String name;
boolean valid;
do {
name = randomAlphaOfLengthBetween(1, 32);
valid = Name.fromUserProvidedName(name, name).error == null;
} while (valid == false);
return name;
}
/**
* Writes the description of instances to a given {@link Path}
*/
private Path writeInstancesTo(Path path) throws IOException {
Iterable<String> instances = Arrays.asList(
"instances:",
" - name: \"node1\"",
" ip:",
" - \"127.0.0.1\"",
" dns: \"localhost\"",
" - name: \"node2\"",
" filename: \"node2\"",
" ip: \"::1\"",
" cn:",
" - \"node2.elasticsearch\"",
" - name: \"node3\"",
" filename: \"node3\"",
" - name: \"CN=different value\"",
" filename: \"different file\"",
" dns:",
" - \"node4.mydomain.com\"");
return Files.write(path, instances, StandardCharsets.UTF_8);
}
@SuppressForbidden(reason = "resolve paths against CWD for a CLI tool")
private static Path resolvePath(String path) {
return PathUtils.get(path).toAbsolutePath();
}
}

View File

@ -5,9 +5,12 @@
*/ */
package org.elasticsearch.xpack.ssl; package org.elasticsearch.xpack.ssl;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509ExtendedKeyManager;
import javax.net.ssl.X509ExtendedTrustManager;
import javax.security.auth.x500.X500Principal; import javax.security.auth.x500.X500Principal;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.Reader; import java.io.Reader;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.URI; import java.net.URI;
@ -20,6 +23,7 @@ import java.nio.file.attribute.PosixFilePermission;
import java.security.Key; import java.security.Key;
import java.security.KeyPair; import java.security.KeyPair;
import java.security.KeyStore; import java.security.KeyStore;
import java.security.Principal;
import java.security.PrivateKey; import java.security.PrivateKey;
import java.security.cert.Certificate; import java.security.cert.Certificate;
import java.security.cert.CertificateFactory; import java.security.cert.CertificateFactory;
@ -40,6 +44,8 @@ import java.util.stream.Collectors;
import com.google.common.jimfs.Configuration; import com.google.common.jimfs.Configuration;
import com.google.common.jimfs.Jimfs; import com.google.common.jimfs.Jimfs;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import org.apache.lucene.util.IOUtils; import org.apache.lucene.util.IOUtils;
import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1Sequence; import org.bouncycastle.asn1.ASN1Sequence;
@ -63,21 +69,26 @@ import org.elasticsearch.common.SuppressForbidden;
import org.elasticsearch.common.io.PathUtils; import org.elasticsearch.common.io.PathUtils;
import org.elasticsearch.common.network.NetworkAddress; import org.elasticsearch.common.network.NetworkAddress;
import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.CollectionUtils;
import org.elasticsearch.env.Environment; import org.elasticsearch.env.Environment;
import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.test.SecuritySettingsSource; import org.elasticsearch.test.SecuritySettingsSource;
import org.elasticsearch.xpack.ssl.CertificateTool.CAInfo; import org.elasticsearch.xpack.ssl.CertificateTool.CAInfo;
import org.elasticsearch.xpack.ssl.CertificateTool.CertificateAuthorityCommand;
import org.elasticsearch.xpack.ssl.CertificateTool.CertificateCommand;
import org.elasticsearch.xpack.ssl.CertificateTool.CertificateInformation; import org.elasticsearch.xpack.ssl.CertificateTool.CertificateInformation;
import org.elasticsearch.xpack.ssl.CertificateTool.GenerateCertificateCommand;
import org.elasticsearch.xpack.ssl.CertificateTool.Name; import org.elasticsearch.xpack.ssl.CertificateTool.Name;
import org.hamcrest.Matchers; import org.hamcrest.Matchers;
import org.junit.After; import org.junit.After;
import static org.elasticsearch.test.TestMatchers.pathExists; import static org.elasticsearch.test.TestMatchers.pathExists;
import static org.hamcrest.Matchers.arrayWithSize;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue;
/** /**
* Unit tests for the tool used to simplify SSL certificate generation * Unit tests for the tool used to simplify SSL certificate generation
@ -106,16 +117,16 @@ public class CertificateToolTests extends ESTestCase {
Path outputFile = outputDir.resolve("certs.zip"); Path outputFile = outputDir.resolve("certs.zip");
MockTerminal terminal = new MockTerminal(); MockTerminal terminal = new MockTerminal();
// test with a user provided dir // test with a user provided file
Path resolvedOutputFile = CertificateTool.getOutputFile(terminal, outputFile.toString(), null); Path resolvedOutputFile = CertificateCommand.resolveOutputPath(terminal, outputFile.toString(), "something");
assertEquals(outputFile, resolvedOutputFile); assertEquals(outputFile, resolvedOutputFile);
assertTrue(terminal.getOutput().isEmpty()); assertTrue(terminal.getOutput().isEmpty());
// test without a user provided directory // test without a user provided file, with user input (prompted)
Path userPromptedOutputFile = outputDir.resolve("csr"); Path userPromptedOutputFile = outputDir.resolve("csr");
assertFalse(Files.exists(userPromptedOutputFile)); assertFalse(Files.exists(userPromptedOutputFile));
terminal.addTextInput(userPromptedOutputFile.toString()); terminal.addTextInput(userPromptedOutputFile.toString());
resolvedOutputFile = CertificateTool.getOutputFile(terminal, null, "out.zip"); resolvedOutputFile = CertificateCommand.resolveOutputPath(terminal, (String) null, "default.zip");
assertEquals(userPromptedOutputFile, resolvedOutputFile); assertEquals(userPromptedOutputFile, resolvedOutputFile);
assertTrue(terminal.getOutput().isEmpty()); assertTrue(terminal.getOutput().isEmpty());
@ -123,7 +134,7 @@ public class CertificateToolTests extends ESTestCase {
String defaultFilename = randomAlphaOfLengthBetween(1, 10); String defaultFilename = randomAlphaOfLengthBetween(1, 10);
Path expectedDefaultPath = resolvePath(defaultFilename); Path expectedDefaultPath = resolvePath(defaultFilename);
terminal.addTextInput(""); terminal.addTextInput("");
resolvedOutputFile = CertificateTool.getOutputFile(terminal, null, defaultFilename); resolvedOutputFile = CertificateCommand.resolveOutputPath(terminal, (String) null, defaultFilename);
assertEquals(expectedDefaultPath, resolvedOutputFile); assertEquals(expectedDefaultPath, resolvedOutputFile);
assertTrue(terminal.getOutput().isEmpty()); assertTrue(terminal.getOutput().isEmpty());
} }
@ -162,7 +173,7 @@ public class CertificateToolTests extends ESTestCase {
} }
} }
Collection<CertificateInformation> certInfos = CertificateTool.getCertificateInformationList(terminal, null); Collection<CertificateInformation> certInfos = CertificateCommand.readMultipleCertificateInformation(terminal);
logger.info("certificate tool output:\n{}", terminal.getOutput()); logger.info("certificate tool output:\n{}", terminal.getOutput());
assertEquals(numberOfInstances, certInfos.size()); assertEquals(numberOfInstances, certInfos.size());
for (CertificateInformation certInfo : certInfos) { for (CertificateInformation certInfo : certInfos) {
@ -233,7 +244,9 @@ public class CertificateToolTests extends ESTestCase {
assertEquals(4, certInfos.size()); assertEquals(4, certInfos.size());
assertFalse(Files.exists(outputFile)); assertFalse(Files.exists(outputFile));
CertificateTool.generateAndWriteCsrs(outputFile, certInfos, randomFrom(1024, 2048)); int keySize = randomFrom(1024, 2048);
new CertificateTool.SigningRequestCommand().generateAndWriteCsrs(outputFile, keySize, certInfos);
assertTrue(Files.exists(outputFile)); assertTrue(Files.exists(outputFile));
Set<PosixFilePermission> perms = Files.getPosixFilePermissions(outputFile); Set<PosixFilePermission> perms = Files.getPosixFilePermissions(outputFile);
@ -265,24 +278,37 @@ public class CertificateToolTests extends ESTestCase {
} }
} }
public void testGeneratingSignedCertificates() throws Exception { public void testGeneratingSignedPemCertificates() throws Exception {
Path tempDir = initTempDir(); Path tempDir = initTempDir();
Path outputFile = tempDir.resolve("out.zip"); Path outputFile = tempDir.resolve("out.zip");
Path instanceFile = writeInstancesTo(tempDir.resolve("instances.yml")); Path instanceFile = writeInstancesTo(tempDir.resolve("instances.yml"));
Collection<CertificateInformation> certInfos = CertificateTool.parseFile(instanceFile); Collection<CertificateInformation> certInfos = CertificateTool.parseFile(instanceFile);
assertEquals(4, certInfos.size()); assertEquals(4, certInfos.size());
final int keysize = randomFrom(1024, 2048); int keySize = randomFrom(1024, 2048);
final int days = randomIntBetween(1, 1024); int days = randomIntBetween(1, 1024);
KeyPair keyPair = CertUtils.generateKeyPair(keysize);
KeyPair keyPair = CertUtils.generateKeyPair(keySize);
X509Certificate caCert = CertUtils.generateCACertificate(new X500Principal("CN=test ca"), keyPair, days); X509Certificate caCert = CertUtils.generateCACertificate(new X500Principal("CN=test ca"), keyPair, days);
final boolean generatedCa = randomBoolean(); final boolean generatedCa = randomBoolean();
final char[] keyPassword = randomBoolean() ? SecuritySettingsSource.TEST_PASSWORD.toCharArray() : null; final boolean keepCaKey = generatedCa && randomBoolean();
final char[] pkcs12Password = randomBoolean() ? randomAlphaOfLengthBetween(1, 12).toCharArray() : null; final String keyPassword = randomBoolean() ? SecuritySettingsSource.TEST_PASSWORD : null;
assertFalse(Files.exists(outputFile)); assertFalse(Files.exists(outputFile));
CAInfo caInfo = new CAInfo(caCert, keyPair.getPrivate(), generatedCa, keyPassword); CAInfo caInfo = new CAInfo(caCert, keyPair.getPrivate(), generatedCa, keyPassword == null ? null : keyPassword.toCharArray());
CertificateTool.generateAndWriteSignedCertificates(outputFile, certInfos, caInfo, keysize, days, pkcs12Password); final GenerateCertificateCommand command = new GenerateCertificateCommand();
List<String> args = CollectionUtils.arrayAsArrayList("-keysize", String.valueOf(keySize), "-days", String.valueOf(days), "-pem");
if (keyPassword != null) {
args.add("-pass");
args.add(keyPassword);
}
if (keepCaKey) {
args.add("-keep-ca-key");
}
final OptionSet options = command.getParser().parse(Strings.toStringArray(args));
command.generateAndWriteSignedCertificates(outputFile, true, options, certInfos, caInfo, null);
assertTrue(Files.exists(outputFile)); assertTrue(Files.exists(outputFile));
Set<PosixFilePermission> perms = Files.getPosixFilePermissions(outputFile); Set<PosixFilePermission> perms = Files.getPosixFilePermissions(outputFile);
@ -296,32 +322,33 @@ public class CertificateToolTests extends ESTestCase {
if (generatedCa) { if (generatedCa) {
assertTrue(Files.exists(zipRoot.resolve("ca"))); assertTrue(Files.exists(zipRoot.resolve("ca")));
assertTrue(Files.exists(zipRoot.resolve("ca").resolve("ca.crt"))); assertTrue(Files.exists(zipRoot.resolve("ca").resolve("ca.crt")));
assertTrue(Files.exists(zipRoot.resolve("ca").resolve("ca.key")));
// check the CA cert // check the CA cert
try (Reader reader = Files.newBufferedReader(zipRoot.resolve("ca").resolve("ca.crt"))) { try (Reader reader = Files.newBufferedReader(zipRoot.resolve("ca").resolve("ca.crt"))) {
X509Certificate parsedCaCert = readX509Certificate(reader); X509Certificate parsedCaCert = readX509Certificate(reader);
assertThat(parsedCaCert.getSubjectX500Principal().getName(), containsString("test ca")); assertThat(parsedCaCert.getSubjectX500Principal().getName(), containsString("test ca"));
assertEquals(caCert, parsedCaCert); assertEquals(caCert, parsedCaCert);
long daysBetween = ChronoUnit.DAYS.between(caCert.getNotBefore().toInstant(), caCert.getNotAfter().toInstant()); long daysBetween = getDurationInDays(caCert);
assertEquals(days, (int) daysBetween); assertEquals(days, (int) daysBetween);
} }
// check the CA key if (keepCaKey) {
if (keyPassword != null) { assertTrue(Files.exists(zipRoot.resolve("ca").resolve("ca.key")));
try (Reader reader = Files.newBufferedReader(zipRoot.resolve("ca").resolve("ca.key"))) { // check the CA key
PEMParser pemParser = new PEMParser(reader); if (keyPassword != null) {
Object parsed = pemParser.readObject(); try (Reader reader = Files.newBufferedReader(zipRoot.resolve("ca").resolve("ca.key"))) {
assertThat(parsed, instanceOf(PEMEncryptedKeyPair.class)); PEMParser pemParser = new PEMParser(reader);
char[] zeroChars = new char[keyPassword.length]; Object parsed = pemParser.readObject();
Arrays.fill(zeroChars, (char) 0); assertThat(parsed, instanceOf(PEMEncryptedKeyPair.class));
assertArrayEquals(zeroChars, keyPassword); char[] zeroChars = new char[caInfo.password.length];
Arrays.fill(zeroChars, (char) 0);
assertArrayEquals(zeroChars, caInfo.password);
}
} }
}
try (Reader reader = Files.newBufferedReader(zipRoot.resolve("ca").resolve("ca.key"))) { try (Reader reader = Files.newBufferedReader(zipRoot.resolve("ca").resolve("ca.key"))) {
PrivateKey privateKey = CertUtils.readPrivateKey(reader, () -> keyPassword != null ? PrivateKey privateKey = CertUtils.readPrivateKey(reader, () -> keyPassword != null ? keyPassword.toCharArray() : null);
SecuritySettingsSource.TEST_PASSWORD.toCharArray() : null); assertEquals(caInfo.certAndKey.key, privateKey);
assertEquals(caInfo.privateKey, privateKey); }
} }
} else { } else {
assertFalse(Files.exists(zipRoot.resolve("ca"))); assertFalse(Files.exists(zipRoot.resolve("ca")));
@ -346,20 +373,7 @@ public class CertificateToolTests extends ESTestCase {
GeneralNames.fromExtensions(x509CertHolder.getExtensions(), Extension.subjectAlternativeName); GeneralNames.fromExtensions(x509CertHolder.getExtensions(), Extension.subjectAlternativeName);
assertSubjAltNames(subjAltNames, certInfo); assertSubjAltNames(subjAltNames, certInfo);
} }
if (pkcs12Password != null) { assertThat(p12, not(pathExists(p12)));
assertThat(p12, pathExists(p12));
try (InputStream in = Files.newInputStream(p12)) {
final KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(in, pkcs12Password);
final Certificate p12Certificate = ks.getCertificate(certInfo.name.originalName);
assertThat("Certificate " + certInfo.name, p12Certificate, notNullValue());
assertThat(p12Certificate, equalTo(certificate));
final Key key = ks.getKey(certInfo.name.originalName, pkcs12Password);
assertThat(key, notNullValue());
}
} else {
assertThat(p12, not(pathExists(p12)));
}
} }
} }
} }
@ -374,38 +388,64 @@ public class CertificateToolTests extends ESTestCase {
terminal.addSecretInput("testnode"); terminal.addSecretInput("testnode");
} }
final int keySize = randomFrom(1024, 2048);
final int days = randomIntBetween(1, 1024); final int days = randomIntBetween(1, 1024);
CAInfo caInfo = CertificateTool.getCAInfo(terminal, "CN=foo", testNodeCertPath.toString(), testNodeKeyPath.toString(), String caPassword = passwordPrompt ? null : "testnode";
passwordPrompt ? null : "testnode".toCharArray(), passwordPrompt, env, randomFrom(1024, 2048), days);
List<String> args = CollectionUtils.arrayAsArrayList(
"-keysize", String.valueOf(keySize),
"-days", String.valueOf(days),
"-pem",
"-ca-cert", testNodeCertPath.toString(),
"-ca-key", testNodeKeyPath.toString());
args.add("-ca-pass");
if (caPassword != null) {
args.add(caPassword);
}
final GenerateCertificateCommand command = new GenerateCertificateCommand();
OptionSet options = command.getParser().parse(Strings.toStringArray(args));
CAInfo caInfo = command.getCAInfo(terminal, options, env);
assertTrue(terminal.getOutput().isEmpty()); assertTrue(terminal.getOutput().isEmpty());
assertEquals(caInfo.caCert.getSubjectX500Principal().getName(), CertificateTool.CertificateAndKey caCK = caInfo.certAndKey;
"CN=Elasticsearch Test Node,OU=elasticsearch,O=org"); assertEquals(caCK.cert.getSubjectX500Principal().getName(), "CN=Elasticsearch Test Node,OU=elasticsearch,O=org");
assertThat(caInfo.privateKey.getAlgorithm(), containsString("RSA")); assertThat(caCK.key.getAlgorithm(), containsString("RSA"));
assertEquals(2048, ((RSAKey) caInfo.privateKey).getModulus().bitLength()); assertEquals(2048, ((RSAKey) caCK.key).getModulus().bitLength());
assertFalse(caInfo.generated); assertFalse(caInfo.generated);
long daysBetween = ChronoUnit.DAYS.between(caInfo.caCert.getNotBefore().toInstant(), caInfo.caCert.getNotAfter().toInstant()); long daysBetween = getDurationInDays(caCK.cert);
assertEquals(1460L, daysBetween); assertEquals(1460L, daysBetween);
// test generation // test generation
args = CollectionUtils.arrayAsArrayList(
"-keysize", String.valueOf(keySize),
"-days", String.valueOf(days),
"-pem",
"-ca-dn", "CN=foo bar");
final boolean passwordProtected = randomBoolean(); final boolean passwordProtected = randomBoolean();
final char[] password; if (passwordProtected) {
if (passwordPrompt && passwordProtected) { args.add("-ca-pass");
password = null; if (passwordPrompt) {
terminal.addSecretInput("testnode"); terminal.addSecretInput("testnode");
} else { } else {
password = "testnode".toCharArray(); args.add(caPassword);
}
} }
final int keysize = randomFrom(1024, 2048);
caInfo = CertificateTool.getCAInfo(terminal, "CN=foo bar", null, null, password, passwordProtected && passwordPrompt, env, options = command.getParser().parse(Strings.toStringArray(args));
keysize, days); caInfo = command.getCAInfo(terminal, options, env);
caCK = caInfo.certAndKey;
assertTrue(terminal.getOutput().isEmpty()); assertTrue(terminal.getOutput().isEmpty());
assertThat(caInfo.caCert, instanceOf(X509Certificate.class)); assertThat(caCK.cert, instanceOf(X509Certificate.class));
assertEquals(caInfo.caCert.getSubjectX500Principal().getName(), "CN=foo bar"); assertEquals(caCK.cert.getSubjectX500Principal().getName(), "CN=foo bar");
assertThat(caInfo.privateKey.getAlgorithm(), containsString("RSA")); assertThat(caCK.key.getAlgorithm(), containsString("RSA"));
assertTrue(caInfo.generated); assertTrue(caInfo.generated);
assertEquals(keysize, ((RSAKey) caInfo.privateKey).getModulus().bitLength()); assertEquals(keySize, getKeySize(caCK.key));
daysBetween = ChronoUnit.DAYS.between(caInfo.caCert.getNotBefore().toInstant(), caInfo.caCert.getNotAfter().toInstant()); assertEquals(days, getDurationInDays(caCK.cert));
assertEquals(days, (int) daysBetween);
} }
public void testNameValues() throws Exception { public void testNameValues() throws Exception {
@ -416,6 +456,13 @@ public class CertificateToolTests extends ESTestCase {
assertEquals("CN=my instance", name.x500Principal.getName()); assertEquals("CN=my instance", name.x500Principal.getName());
assertEquals("my instance", name.filename); assertEquals("my instance", name.filename);
// null
name = Name.fromUserProvidedName(null, "");
assertEquals("", name.originalName);
assertThat(name.error, containsString("null"));
assertNull(name.x500Principal);
assertNull(name.filename);
// too long // too long
String userProvidedName = randomAlphaOfLength(CertificateTool.MAX_FILENAME_LENGTH + 1); String userProvidedName = randomAlphaOfLength(CertificateTool.MAX_FILENAME_LENGTH + 1);
name = Name.fromUserProvidedName(userProvidedName, userProvidedName); name = Name.fromUserProvidedName(userProvidedName, userProvidedName);
@ -426,7 +473,7 @@ public class CertificateToolTests extends ESTestCase {
name = Name.fromUserProvidedName("", ""); name = Name.fromUserProvidedName("", "");
assertEquals("", name.originalName); assertEquals("", name.originalName);
assertThat(name.error, containsString("valid filename")); assertThat(name.error, containsString("valid filename"));
assertEquals("CN=", name.x500Principal.getName()); assertEquals("CN=", String.valueOf(name.x500Principal));
assertNull(name.filename); assertNull(name.filename);
// invalid characters only // invalid characters only
@ -460,6 +507,260 @@ public class CertificateToolTests extends ESTestCase {
assertEquals("CN=" + userProvidedName, name.x500Principal.getName()); assertEquals("CN=" + userProvidedName, name.x500Principal.getName());
} }
/**
* A multi-stage test that:
* - Create a new CA
* - Uses that CA to create 2 node certificates
* - Creates a 3rd node certificate using an auto-generated CA
* - Checks that the first 2 node certificates trust one another
* - Checks that the 3rd node certificate is _not_ trusted
* - Checks that all 3 certificates have the right values based on the command line options provided during generation
*/
public void testCreateCaAndMultipleInstances() throws Exception {
final Path tempDir = initTempDir();
final Terminal terminal = new MockTerminal();
Environment env = new Environment(Settings.builder().put("path.home", tempDir).build());
final Path caFile = tempDir.resolve("ca.p12");
final Path node1File = tempDir.resolve("node1.p12").toAbsolutePath();
final Path node2File = tempDir.resolve("node2.p12").toAbsolutePath();
final Path node3File = tempDir.resolve("node3.p12").toAbsolutePath();
final int caKeySize = randomIntBetween(4, 8) * 512;
final int node1KeySize = randomIntBetween(2, 6) * 512;
final int node2KeySize = randomIntBetween(2, 6) * 512;
final int node3KeySize = randomIntBetween(1, 4) * 512;
final int days = randomIntBetween(7, 1500);
final String caPassword = randomAlphaOfLengthBetween(4, 16);
final String node1Password = randomAlphaOfLengthBetween(4, 16);
final String node2Password = randomAlphaOfLengthBetween(4, 16);
final String node3Password = randomAlphaOfLengthBetween(4, 16);
final String node1Ip = "200.181." + randomIntBetween(1, 250) + "." + randomIntBetween(1, 250);
final String node2Ip = "200.182." + randomIntBetween(1, 250) + "." + randomIntBetween(1, 250);
final String node3Ip = "200.183." + randomIntBetween(1, 250) + "." + randomIntBetween(1, 250);
final CertificateAuthorityCommand caCommand = new CertificateAuthorityCommand() {
@Override
Path resolveOutputPath(Terminal terminal, OptionSet options, String defaultFilename) throws IOException {
// Needed to work within the security manager
return caFile;
}
};
final OptionSet caOptions = caCommand.getParser().parse(
"-ca-dn", "CN=My ElasticSearch Cluster",
"-pass", caPassword,
"-out", caFile.toString(),
"-keysize", String.valueOf(caKeySize),
"-days", String.valueOf(days)
);
caCommand.execute(terminal, caOptions, env);
assertThat(caFile, pathExists(caFile));
final GenerateCertificateCommand gen1Command = new PathAwareGenerateCertificateCommand(caFile, node1File);
final OptionSet gen1Options = gen1Command.getParser().parse(
"-ca", "<ca>",
"-ca-pass", caPassword,
"-pass", node1Password,
"-out", "<node1>",
"-keysize", String.valueOf(node1KeySize),
"-days", String.valueOf(days),
"-dns", "node01.cluster1.es.internal.corp.net",
"-ip", node1Ip,
"-name", "node01");
gen1Command.execute(terminal, gen1Options, env);
assertThat(node1File, pathExists(node1File));
final GenerateCertificateCommand gen2Command = new PathAwareGenerateCertificateCommand(caFile, node2File);
final OptionSet gen2Options = gen2Command.getParser().parse(
"-ca", "<ca>",
"-ca-pass", caPassword,
"-pass", node2Password,
"-out", "<node2>",
"-keysize", String.valueOf(node2KeySize),
"-days", String.valueOf(days),
"-dns", "node02.cluster1.es.internal.corp.net",
"-ip", node2Ip,
"-name", "node02");
gen2Command.execute(terminal, gen2Options, env);
assertThat(node2File, pathExists(node2File));
// Node 3 uses an auto generated CA, and therefore should not be trusted by the other nodes.
final GenerateCertificateCommand gen3Command = new PathAwareGenerateCertificateCommand(null, node3File);
final OptionSet gen3Options = gen3Command.getParser().parse(
"-ca-dn", "CN=My ElasticSearch Cluster 2",
"-pass", node3Password,
"-out", "<node3>",
"-keysize", String.valueOf(node3KeySize),
"-days", String.valueOf(days),
"-dns", "node03.cluster2.es.internal.corp.net",
"-ip", node3Ip);
gen3Command.execute(terminal, gen3Options, env);
assertThat(node3File, pathExists(node3File));
final KeyStore node1KeyStore = CertUtils.readKeyStore(node1File, "PKCS12", node1Password.toCharArray());
final KeyStore node2KeyStore = CertUtils.readKeyStore(node2File, "PKCS12", node2Password.toCharArray());
final KeyStore node3KeyStore = CertUtils.readKeyStore(node3File, "PKCS12", node3Password.toCharArray());
checkTrust(node1KeyStore, node1Password.toCharArray(), node1KeyStore, true);
checkTrust(node1KeyStore, node1Password.toCharArray(), node2KeyStore, true);
checkTrust(node2KeyStore, node2Password.toCharArray(), node2KeyStore, true);
checkTrust(node2KeyStore, node2Password.toCharArray(), node1KeyStore, true);
checkTrust(node1KeyStore, node1Password.toCharArray(), node3KeyStore, false);
checkTrust(node3KeyStore, node3Password.toCharArray(), node2KeyStore, false);
checkTrust(node3KeyStore, node3Password.toCharArray(), node3KeyStore, true);
final Certificate node1Cert = node1KeyStore.getCertificate("node01");
assertThat(node1Cert, instanceOf(X509Certificate.class));
assertSubjAltNames(node1Cert, node1Ip, "node01.cluster1.es.internal.corp.net");
assertThat(getDurationInDays((X509Certificate) node1Cert), equalTo(days));
final Key node1Key = node1KeyStore.getKey("node01", node1Password.toCharArray());
assertThat(getKeySize(node1Key), equalTo(node1KeySize));
final Certificate node2Cert = node2KeyStore.getCertificate("node02");
assertThat(node2Cert, instanceOf(X509Certificate.class));
assertSubjAltNames(node2Cert, node2Ip, "node02.cluster1.es.internal.corp.net");
assertThat(getDurationInDays((X509Certificate) node2Cert), equalTo(days));
final Key node2Key = node2KeyStore.getKey("node02", node2Password.toCharArray());
assertThat(getKeySize(node2Key), equalTo(node2KeySize));
final Certificate node3Cert = node3KeyStore.getCertificate(CertificateTool.DEFAULT_CERT_NAME);
assertThat(node3Cert, instanceOf(X509Certificate.class));
assertSubjAltNames(node3Cert, node3Ip, "node03.cluster2.es.internal.corp.net");
assertThat(getDurationInDays((X509Certificate) node3Cert), equalTo(days));
final Key node3Key = node3KeyStore.getKey(CertificateTool.DEFAULT_CERT_NAME, node3Password.toCharArray());
assertThat(getKeySize(node3Key), equalTo(node3KeySize));
}
/**
* A multi-stage test that:
* - Creates a ZIP of a PKCS12 cert, with an auto-generated CA
* - Uses the generate CA to create a PEM certificate
* - Checks that the PKCS12 certificate and the PEM certificate trust one another
*/
public void testTrustBetweenPEMandPKCS12() throws Exception {
final Path tempDir = initTempDir();
final MockTerminal terminal = new MockTerminal();
Environment env = new Environment(Settings.builder().put("path.home", tempDir).build());
final Path pkcs12Zip = tempDir.resolve("p12.zip");
final Path pemZip = tempDir.resolve("pem.zip");
final int keySize = randomIntBetween(4, 8) * 512;
final int days = randomIntBetween(500, 1500);
final String caPassword = randomAlphaOfLengthBetween(4, 16);
final String node1Password = randomAlphaOfLengthBetween(4, 16);
final GenerateCertificateCommand gen1Command = new PathAwareGenerateCertificateCommand(null, pkcs12Zip);
final OptionSet gen1Options = gen1Command.getParser().parse(
"-keep-ca-key",
"-out", "<zip>",
"-keysize", String.valueOf(keySize),
"-days", String.valueOf(days),
"-dns", "node01.cluster1.es.internal.corp.net",
"-name", "node01"
);
terminal.addSecretInput(caPassword);
terminal.addSecretInput(node1Password);
gen1Command.execute(terminal, gen1Options, env);
assertThat(pkcs12Zip, pathExists(pkcs12Zip));
FileSystem zip1FS = FileSystems.newFileSystem(new URI("jar:" + pkcs12Zip.toUri()), Collections.emptyMap());
Path zip1Root = zip1FS.getPath("/");
final Path caP12 = zip1Root.resolve("ca/ca.p12");
assertThat(caP12, pathExists(caP12));
final Path node1P12 = zip1Root.resolve("node01/node01.p12");
assertThat(node1P12, pathExists(node1P12));
final GenerateCertificateCommand gen2Command = new PathAwareGenerateCertificateCommand(caP12, pemZip);
final OptionSet gen2Options = gen2Command.getParser().parse(
"-ca", "<ca>",
"-out", "<zip>",
"-keysize", String.valueOf(keySize),
"-days", String.valueOf(days),
"-dns", "node02.cluster1.es.internal.corp.net",
"-name", "node02",
"-pem"
);
terminal.addSecretInput(caPassword);
gen2Command.execute(terminal, gen2Options, env);
assertThat(pemZip, pathExists(pemZip));
FileSystem zip2FS = FileSystems.newFileSystem(new URI("jar:" + pemZip.toUri()), Collections.emptyMap());
Path zip2Root = zip2FS.getPath("/");
final Path ca2 = zip2Root.resolve("ca/ca.p12");
assertThat(ca2, not(pathExists(ca2)));
final Path node2Cert = zip2Root.resolve("node02/node02.crt");
assertThat(node2Cert, pathExists(node2Cert));
final Path node2Key = zip2Root.resolve("node02/node02.key");
assertThat(node2Key, pathExists(node2Key));
final KeyStore node1KeyStore = CertUtils.readKeyStore(node1P12, "PKCS12", node1Password.toCharArray());
final KeyStore node1TrustStore = node1KeyStore;
final KeyStore node2KeyStore = CertUtils.getKeyStoreFromPEM(node2Cert, node2Key, new char[0]);
final KeyStore node2TrustStore = CertUtils.readKeyStore(caP12, "PKCS12", caPassword.toCharArray());
checkTrust(node1KeyStore, node1Password.toCharArray(), node2TrustStore, true);
checkTrust(node2KeyStore, new char[0], node1TrustStore, true);
}
private int getKeySize(Key node1Key) {
assertThat(node1Key, instanceOf(RSAKey.class));
return ((RSAKey) node1Key).getModulus().bitLength();
}
private int getDurationInDays(X509Certificate cert) {
return (int) ChronoUnit.DAYS.between(cert.getNotBefore().toInstant(), cert.getNotAfter().toInstant());
}
private void assertSubjAltNames(Certificate certificate, String ip, String dns) throws Exception {
final X509CertificateHolder holder = new X509CertificateHolder(certificate.getEncoded());
final GeneralNames names = GeneralNames.fromExtensions(holder.getExtensions(), Extension.subjectAlternativeName);
final CertificateInformation certInfo = new CertificateInformation("n", "n", Collections.singletonList(ip),
Collections.singletonList(dns), Collections.emptyList());
assertSubjAltNames(names, certInfo);
}
/**
* Checks whether there are keys in {@code keyStore} that are trusted by {@code trustStore}.
*/
private void checkTrust(KeyStore keyStore, char[] keyPassword, KeyStore trustStore, boolean trust) throws Exception {
final X509ExtendedKeyManager keyManager = CertUtils.keyManager(keyStore, keyPassword, KeyManagerFactory.getDefaultAlgorithm());
final X509ExtendedTrustManager trustManager = CertUtils.trustManager(trustStore, TrustManagerFactory.getDefaultAlgorithm());
final X509Certificate[] node1CertificateIssuers = trustManager.getAcceptedIssuers();
final Principal[] trustedPrincipals = new Principal[node1CertificateIssuers.length];
for (int i = 0; i < node1CertificateIssuers.length; i++) {
trustedPrincipals[i] = node1CertificateIssuers[i].getIssuerX500Principal();
}
final String[] keyAliases = keyManager.getClientAliases("RSA", trustedPrincipals);
if (trust) {
assertThat(keyAliases, arrayWithSize(1));
trustManager.checkClientTrusted(keyManager.getCertificateChain(keyAliases[0]), "RSA");
} else {
assertThat(keyAliases, nullValue());
}
}
private PKCS10CertificationRequest readCertificateRequest(Path path) throws Exception { private PKCS10CertificationRequest readCertificateRequest(Path path) throws Exception {
try (Reader reader = Files.newBufferedReader(path); try (Reader reader = Files.newBufferedReader(path);
PEMParser pemParser = new PEMParser(reader)) { PEMParser pemParser = new PEMParser(reader)) {
@ -557,4 +858,31 @@ public class CertificateToolTests extends ESTestCase {
private static Path resolvePath(String path) { private static Path resolvePath(String path) {
return PathUtils.get(path).toAbsolutePath(); return PathUtils.get(path).toAbsolutePath();
} }
/**
* Converting jimfs Paths into strings and back to paths doesn't work with the security manager.
* This class works around that by sticking with the original path objects
*/
private static class PathAwareGenerateCertificateCommand extends GenerateCertificateCommand {
private final Path caFile;
private final Path outFile;
PathAwareGenerateCertificateCommand(Path caFile, Path outFile) {
this.caFile = caFile;
this.outFile = outFile;
}
@Override
protected Path resolvePath(OptionSet options, OptionSpec<String> spec) {
if (spec.options().contains("ca")) {
return caFile;
}
return super.resolvePath(options, spec);
}
@Override
Path resolveOutputPath(Terminal terminal, OptionSet options, String defaultFilename) throws IOException {
return outFile;
}
}
} }