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 2654 additions and 491 deletions

View File

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

View File

@ -16,7 +16,7 @@ call "%~dp0x-pack-env.bat" || exit /b 1
-Des.path.home="%ES_HOME%" ^
-Des.path.conf="%ES_PATH_CONF%" ^
-cp "%ES_CLASSPATH%" ^
org.elasticsearch.xpack.ssl.CertificateTool ^
org.elasticsearch.xpack.ssl.CertificateGenerateTool ^
%*
endlocal

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 \
"$@"

View File

@ -0,0 +1,23 @@
@echo off
rem Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
rem or more contributor license agreements. Licensed under the Elastic License;
rem you may not use this file except in compliance with the Elastic License.
setlocal enabledelayedexpansion
setlocal enableextensions
call "%~dp0..\elasticsearch-env.bat" || exit /b 1
call "%~dp0x-pack-env.bat" || exit /b 1
%JAVA% ^
%ES_JAVA_OPTS% ^
-Des.path.home="%ES_HOME%" ^
-Des.path.conf="%ES_PATH_CONF%" ^
-cp "%ES_CLASSPATH%" ^
org.elasticsearch.xpack.ssl.CertificateTool ^
%*
endlocal
endlocal

View File

@ -22,6 +22,7 @@ import java.net.SocketException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.Key;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
@ -35,12 +36,18 @@ import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
@ -93,7 +100,8 @@ public class CertUtils {
private static final int SERIAL_BIT_LENGTH = 20 * 8;
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
@ -107,16 +115,36 @@ public class CertUtils {
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
*/
public static X509ExtendedKeyManager keyManager(Certificate[] certificateChain, PrivateKey privateKey, char[] password)
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.load(null, null);
// password must be non-null for keystore...
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
*
* @param certificates the certificates to trust
* @return a trust manager that trusts the provided certificates
*/
public static X509ExtendedTrustManager trustManager(Certificate[] certificates)
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");
store.load(null, null);
int counter = 0;
@ -150,25 +185,32 @@ public class CertUtils {
store.setCertificateEntry("cert" + counter, certificate);
counter++;
}
return trustManager(store, TrustManagerFactory.getDefaultAlgorithm());
return store;
}
/**
* 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 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
*/
public static X509ExtendedTrustManager trustManager(String trustStorePath, String trustStoreType, char[] trustStorePassword,
String trustStoreAlgorithm, @Nullable Environment env)
throws NoSuchAlgorithmException, UnrecoverableKeyException, KeyStoreException, IOException, CertificateException {
try (InputStream in = Files.newInputStream(resolvePath(trustStorePath, env))) {
KeyStore trustStore = KeyStore.getInstance(trustStoreType);
assert trustStorePassword != null;
trustStore.load(in, trustStorePassword);
return trustManager(trustStore, trustStoreAlgorithm);
KeyStore trustStore = readKeyStore(resolvePath(trustStorePath, env), trustStoreType, 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();
for (TrustManager trustManager : trustManagers) {
if (trustManager instanceof X509ExtendedTrustManager) {
return (X509ExtendedTrustManager) trustManager ;
return (X509ExtendedTrustManager) trustManager;
}
}
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
* @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}
* @return an array of {@link Certificate} objects
*/
public static Certificate[] readCertificates(List<String> certPaths, @Nullable Environment environment)
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());
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
for (String path : certPaths) {
try (Reader reader = Files.newBufferedReader(resolvePath(path, environment), StandardCharsets.UTF_8)) {
for (Path path : certPaths) {
try (Reader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
readCertificates(reader, certificates, certFactory);
}
}
@ -280,6 +328,30 @@ public class CertUtils {
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
*/
@ -292,24 +364,25 @@ public class CertUtils {
* 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,
X509Certificate caCert, PrivateKey caPrivKey, int days)
X509Certificate caCert, PrivateKey caPrivKey, int days)
throws OperatorCreationException, CertificateException, CertIOException, NoSuchAlgorithmException {
return generateSignedCertificate(principal, subjectAltNames, keyPair, caCert, caPrivKey, false, days);
}
/**
* 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
* {@code null}
* @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 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 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 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
* @return a signed {@link X509Certificate}
*/
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 {
final DateTime notBefore = new DateTime(DateTimeZone.UTC);
if (days < 1) {
@ -353,10 +426,11 @@ public class CertUtils {
/**
* 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 sanList the subject alternative names that should be added to the certificate as an X509v3 extension. May be
* {@code null}
* @param sanList the subject alternative names that should be added to the certificate as an X509v3 extension. May be
* {@code null}
* @return a certificate signing request
*/
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 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
public boolean matches(Object item) {
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;
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 java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.net.InetAddress;
import java.net.URI;
@ -20,6 +23,7 @@ import java.nio.file.attribute.PosixFilePermission;
import java.security.Key;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.cert.Certificate;
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.Jimfs;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import org.apache.lucene.util.IOUtils;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1Sequence;
@ -63,21 +69,26 @@ 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.common.util.CollectionUtils;
import org.elasticsearch.env.Environment;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.test.SecuritySettingsSource;
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.GenerateCertificateCommand;
import org.elasticsearch.xpack.ssl.CertificateTool.Name;
import org.hamcrest.Matchers;
import org.junit.After;
import static org.elasticsearch.test.TestMatchers.pathExists;
import static org.hamcrest.Matchers.arrayWithSize;
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;
import static org.hamcrest.Matchers.nullValue;
/**
* 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");
MockTerminal terminal = new MockTerminal();
// test with a user provided dir
Path resolvedOutputFile = CertificateTool.getOutputFile(terminal, outputFile.toString(), null);
// test with a user provided file
Path resolvedOutputFile = CertificateCommand.resolveOutputPath(terminal, outputFile.toString(), "something");
assertEquals(outputFile, resolvedOutputFile);
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");
assertFalse(Files.exists(userPromptedOutputFile));
terminal.addTextInput(userPromptedOutputFile.toString());
resolvedOutputFile = CertificateTool.getOutputFile(terminal, null, "out.zip");
resolvedOutputFile = CertificateCommand.resolveOutputPath(terminal, (String) null, "default.zip");
assertEquals(userPromptedOutputFile, resolvedOutputFile);
assertTrue(terminal.getOutput().isEmpty());
@ -123,7 +134,7 @@ public class CertificateToolTests extends ESTestCase {
String defaultFilename = randomAlphaOfLengthBetween(1, 10);
Path expectedDefaultPath = resolvePath(defaultFilename);
terminal.addTextInput("");
resolvedOutputFile = CertificateTool.getOutputFile(terminal, null, defaultFilename);
resolvedOutputFile = CertificateCommand.resolveOutputPath(terminal, (String) null, defaultFilename);
assertEquals(expectedDefaultPath, resolvedOutputFile);
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());
assertEquals(numberOfInstances, certInfos.size());
for (CertificateInformation certInfo : certInfos) {
@ -233,7 +244,9 @@ public class CertificateToolTests extends ESTestCase {
assertEquals(4, certInfos.size());
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));
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 outputFile = tempDir.resolve("out.zip");
Path instanceFile = writeInstancesTo(tempDir.resolve("instances.yml"));
Collection<CertificateInformation> certInfos = CertificateTool.parseFile(instanceFile);
assertEquals(4, certInfos.size());
final int keysize = randomFrom(1024, 2048);
final int days = randomIntBetween(1, 1024);
KeyPair keyPair = CertUtils.generateKeyPair(keysize);
int keySize = randomFrom(1024, 2048);
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;
final boolean keepCaKey = generatedCa && randomBoolean();
final String keyPassword = randomBoolean() ? SecuritySettingsSource.TEST_PASSWORD : null;
assertFalse(Files.exists(outputFile));
CAInfo caInfo = new CAInfo(caCert, keyPair.getPrivate(), generatedCa, keyPassword);
CertificateTool.generateAndWriteSignedCertificates(outputFile, certInfos, caInfo, keysize, days, pkcs12Password);
CAInfo caInfo = new CAInfo(caCert, keyPair.getPrivate(), generatedCa, keyPassword == null ? null : keyPassword.toCharArray());
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));
Set<PosixFilePermission> perms = Files.getPosixFilePermissions(outputFile);
@ -296,32 +322,33 @@ public class CertificateToolTests extends ESTestCase {
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());
long daysBetween = getDurationInDays(caCert);
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);
if (keepCaKey) {
assertTrue(Files.exists(zipRoot.resolve("ca").resolve("ca.key")));
// 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[caInfo.password.length];
Arrays.fill(zeroChars, (char) 0);
assertArrayEquals(zeroChars, caInfo.password);
}
}
}
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);
try (Reader reader = Files.newBufferedReader(zipRoot.resolve("ca").resolve("ca.key"))) {
PrivateKey privateKey = CertUtils.readPrivateKey(reader, () -> keyPassword != null ? keyPassword.toCharArray() : null);
assertEquals(caInfo.certAndKey.key, privateKey);
}
}
} else {
assertFalse(Files.exists(zipRoot.resolve("ca")));
@ -346,20 +373,7 @@ public class CertificateToolTests extends ESTestCase {
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)));
}
assertThat(p12, not(pathExists(p12)));
}
}
}
@ -374,38 +388,64 @@ public class CertificateToolTests extends ESTestCase {
terminal.addSecretInput("testnode");
}
final int keySize = randomFrom(1024, 2048);
final int days = randomIntBetween(1, 1024);
CAInfo caInfo = CertificateTool.getCAInfo(terminal, "CN=foo", testNodeCertPath.toString(), testNodeKeyPath.toString(),
passwordPrompt ? null : "testnode".toCharArray(), passwordPrompt, env, randomFrom(1024, 2048), days);
String caPassword = passwordPrompt ? null : "testnode";
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());
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());
CertificateTool.CertificateAndKey caCK = caInfo.certAndKey;
assertEquals(caCK.cert.getSubjectX500Principal().getName(), "CN=Elasticsearch Test Node,OU=elasticsearch,O=org");
assertThat(caCK.key.getAlgorithm(), containsString("RSA"));
assertEquals(2048, ((RSAKey) caCK.key).getModulus().bitLength());
assertFalse(caInfo.generated);
long daysBetween = ChronoUnit.DAYS.between(caInfo.caCert.getNotBefore().toInstant(), caInfo.caCert.getNotAfter().toInstant());
long daysBetween = getDurationInDays(caCK.cert);
assertEquals(1460L, daysBetween);
// test generation
args = CollectionUtils.arrayAsArrayList(
"-keysize", String.valueOf(keySize),
"-days", String.valueOf(days),
"-pem",
"-ca-dn", "CN=foo bar");
final boolean passwordProtected = randomBoolean();
final char[] password;
if (passwordPrompt && passwordProtected) {
password = null;
terminal.addSecretInput("testnode");
} else {
password = "testnode".toCharArray();
if (passwordProtected) {
args.add("-ca-pass");
if (passwordPrompt) {
terminal.addSecretInput("testnode");
} else {
args.add(caPassword);
}
}
final int keysize = randomFrom(1024, 2048);
caInfo = CertificateTool.getCAInfo(terminal, "CN=foo bar", null, null, password, passwordProtected && passwordPrompt, env,
keysize, days);
options = command.getParser().parse(Strings.toStringArray(args));
caInfo = command.getCAInfo(terminal, options, env);
caCK = caInfo.certAndKey;
assertTrue(terminal.getOutput().isEmpty());
assertThat(caInfo.caCert, instanceOf(X509Certificate.class));
assertEquals(caInfo.caCert.getSubjectX500Principal().getName(), "CN=foo bar");
assertThat(caInfo.privateKey.getAlgorithm(), containsString("RSA"));
assertThat(caCK.cert, instanceOf(X509Certificate.class));
assertEquals(caCK.cert.getSubjectX500Principal().getName(), "CN=foo bar");
assertThat(caCK.key.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);
assertEquals(keySize, getKeySize(caCK.key));
assertEquals(days, getDurationInDays(caCK.cert));
}
public void testNameValues() throws Exception {
@ -416,6 +456,13 @@ public class CertificateToolTests extends ESTestCase {
assertEquals("CN=my instance", name.x500Principal.getName());
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
String userProvidedName = randomAlphaOfLength(CertificateTool.MAX_FILENAME_LENGTH + 1);
name = Name.fromUserProvidedName(userProvidedName, userProvidedName);
@ -426,7 +473,7 @@ public class CertificateToolTests extends ESTestCase {
name = Name.fromUserProvidedName("", "");
assertEquals("", name.originalName);
assertThat(name.error, containsString("valid filename"));
assertEquals("CN=", name.x500Principal.getName());
assertEquals("CN=", String.valueOf(name.x500Principal));
assertNull(name.filename);
// invalid characters only
@ -460,6 +507,260 @@ public class CertificateToolTests extends ESTestCase {
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 {
try (Reader reader = Files.newBufferedReader(path);
PEMParser pemParser = new PEMParser(reader)) {
@ -557,4 +858,31 @@ public class CertificateToolTests extends ESTestCase {
private static Path resolvePath(String path) {
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;
}
}
}