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:
parent
ba29971323
commit
0c7caabea1
|
@ -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 \
|
||||
"$@"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 \
|
||||
"$@"
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue