|
@@ -3,7 +3,7 @@
|
|
|
* 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.core.ssl;
|
|
|
+package org.elasticsearch.xpack.security.cli;
|
|
|
|
|
|
import joptsimple.OptionParser;
|
|
|
import joptsimple.OptionSet;
|
|
@@ -39,6 +39,8 @@ import org.elasticsearch.common.xcontent.ObjectParser;
|
|
|
import org.elasticsearch.common.xcontent.XContentParser;
|
|
|
import org.elasticsearch.common.xcontent.XContentType;
|
|
|
import org.elasticsearch.env.Environment;
|
|
|
+import org.elasticsearch.xpack.core.ssl.CertParsingUtils;
|
|
|
+import org.elasticsearch.xpack.core.ssl.PemUtils;
|
|
|
|
|
|
import javax.security.auth.x500.X500Principal;
|
|
|
|
|
@@ -101,7 +103,7 @@ public class CertificateTool extends LoggingAwareMultiCommand {
|
|
|
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 + "}");
|
|
|
+ Pattern.compile("[a-zA-Z0-9!@#$%^&{}\\[\\]()_+\\-=,.~'` ]{1," + MAX_FILENAME_LENGTH + "}");
|
|
|
private static final int DEFAULT_KEY_SIZE = 2048;
|
|
|
|
|
|
/**
|
|
@@ -115,11 +117,11 @@ public class CertificateTool extends LoggingAwareMultiCommand {
|
|
|
// 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]));
|
|
|
+ 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"));
|
|
@@ -144,28 +146,28 @@ public class CertificateTool extends LoggingAwareMultiCommand {
|
|
|
|
|
|
|
|
|
static final String INTRO_TEXT = "This tool assists you in the generation of X.509 certificates and certificate\n" +
|
|
|
- "signing requests for use with SSL/TLS in the Elastic stack.";
|
|
|
+ "signing requests for use with SSL/TLS in the Elastic stack.";
|
|
|
|
|
|
static final String INSTANCE_EXPLANATION =
|
|
|
- " * An instance is any piece of the Elastic Stack that requires a SSL certificate.\n" +
|
|
|
- " Depending on your configuration, Elasticsearch, Logstash, Kibana, and Beats\n" +
|
|
|
- " may all require a certificate and private key.\n" +
|
|
|
- " * The minimum required value for each instance is a name. This can simply be the\n" +
|
|
|
- " hostname, which will be used as the Common Name of the certificate. A full\n" +
|
|
|
- " distinguished name may also be used.\n" +
|
|
|
- " * A filename value may be required for each instance. This is necessary when the\n" +
|
|
|
- " name would result in an invalid file or directory name. The name provided here\n" +
|
|
|
- " is used as the directory name (within the zip) and the prefix for the key and\n" +
|
|
|
- " certificate files. The filename is required if you are prompted and the name\n" +
|
|
|
- " is not displayed in the prompt.\n" +
|
|
|
- " * IP addresses and DNS names are optional. Multiple values can be specified as a\n" +
|
|
|
- " comma separated string. If no IP addresses or DNS names are provided, you may\n" +
|
|
|
- " disable hostname verification in your SSL configuration.";
|
|
|
+ " * An instance is any piece of the Elastic Stack that requires a SSL certificate.\n" +
|
|
|
+ " Depending on your configuration, Elasticsearch, Logstash, Kibana, and Beats\n" +
|
|
|
+ " may all require a certificate and private key.\n" +
|
|
|
+ " * The minimum required value for each instance is a name. This can simply be the\n" +
|
|
|
+ " hostname, which will be used as the Common Name of the certificate. A full\n" +
|
|
|
+ " distinguished name may also be used.\n" +
|
|
|
+ " * A filename value may be required for each instance. This is necessary when the\n" +
|
|
|
+ " name would result in an invalid file or directory name. The name provided here\n" +
|
|
|
+ " is used as the directory name (within the zip) and the prefix for the key and\n" +
|
|
|
+ " certificate files. The filename is required if you are prompted and the name\n" +
|
|
|
+ " is not displayed in the prompt.\n" +
|
|
|
+ " * IP addresses and DNS names are optional. Multiple values can be specified as a\n" +
|
|
|
+ " comma separated string. If no IP addresses or DNS names are provided, you may\n" +
|
|
|
+ " disable hostname verification in your SSL configuration.";
|
|
|
|
|
|
static final String CA_EXPLANATION =
|
|
|
- " * All certificates generated by this tool will be signed by a certificate authority (CA).\n" +
|
|
|
- " * The tool can automatically generate a new CA for you, or you can provide your own with the\n" +
|
|
|
- " -ca or -ca-cert command line options.";
|
|
|
+ " * All certificates generated by this tool will be signed by a certificate authority (CA).\n" +
|
|
|
+ " * The tool can automatically generate a new CA for you, or you can provide your own with the\n" +
|
|
|
+ " -ca or -ca-cert command line options.";
|
|
|
|
|
|
|
|
|
abstract static class CertificateCommand extends EnvironmentAwareCommand {
|
|
@@ -202,32 +204,32 @@ public class CertificateTool extends LoggingAwareMultiCommand {
|
|
|
final void acceptCertificateGenerationOptions() {
|
|
|
pemFormatSpec = parser.accepts("pem", "output certificates and keys in PEM format instead of PKCS#12");
|
|
|
daysSpec = parser.accepts("days", "number of days that the generated certificates are valid")
|
|
|
- .withRequiredArg().ofType(Integer.class);
|
|
|
+ .withRequiredArg().ofType(Integer.class);
|
|
|
}
|
|
|
|
|
|
final void acceptsCertificateAuthority() {
|
|
|
caPkcs12PathSpec = parser.accepts("ca", "path to an existing ca key pair (in PKCS#12 format)").withRequiredArg();
|
|
|
caCertPathSpec = parser.accepts("ca-cert", "path to an existing ca certificate")
|
|
|
- .availableUnless(caPkcs12PathSpec)
|
|
|
- .withRequiredArg();
|
|
|
+ .availableUnless(caPkcs12PathSpec)
|
|
|
+ .withRequiredArg();
|
|
|
caKeyPathSpec = parser.accepts("ca-key", "path to an existing ca private key")
|
|
|
- .availableIf(caCertPathSpec)
|
|
|
- .requiredIf(caCertPathSpec)
|
|
|
- .withRequiredArg();
|
|
|
+ .availableIf(caCertPathSpec)
|
|
|
+ .requiredIf(caCertPathSpec)
|
|
|
+ .withRequiredArg();
|
|
|
|
|
|
keepCaKeySpec = parser.accepts("keep-ca-key", "retain the CA private key for future use")
|
|
|
- .availableUnless(caPkcs12PathSpec)
|
|
|
- .availableUnless(caCertPathSpec);
|
|
|
+ .availableUnless(caPkcs12PathSpec)
|
|
|
+ .availableUnless(caCertPathSpec);
|
|
|
|
|
|
caPasswordSpec = parser.accepts("ca-pass", "password for an existing ca private key or the generated ca private key")
|
|
|
- .withOptionalArg();
|
|
|
+ .withOptionalArg();
|
|
|
|
|
|
acceptsCertificateAuthorityName();
|
|
|
}
|
|
|
|
|
|
void acceptsCertificateAuthorityName() {
|
|
|
OptionSpecBuilder builder = parser.accepts("ca-dn",
|
|
|
- "distinguished name to use for the generated ca. defaults to " + AUTO_GEN_CA_DN);
|
|
|
+ "distinguished name to use for the generated ca. defaults to " + AUTO_GEN_CA_DN);
|
|
|
if (caPkcs12PathSpec != null) {
|
|
|
builder = builder.availableUnless(caPkcs12PathSpec);
|
|
|
}
|
|
@@ -336,11 +338,11 @@ public class CertificateTool extends LoggingAwareMultiCommand {
|
|
|
char[] passwordOption = getChars(caPasswordSpec.value(options));
|
|
|
|
|
|
Map<Certificate, Key> keys = withPassword("CA (" + path + ")", passwordOption,
|
|
|
- terminal, password -> CertParsingUtils.readPkcs12KeyPairs(path, password, a -> password));
|
|
|
+ terminal, password -> CertParsingUtils.readPkcs12KeyPairs(path, password, a -> password));
|
|
|
|
|
|
if (keys.size() != 1) {
|
|
|
throw new IllegalArgumentException("expected a single key in file [" + path.toAbsolutePath() + "] but found [" +
|
|
|
- keys.size() + "]");
|
|
|
+ keys.size() + "]");
|
|
|
}
|
|
|
final Map.Entry<Certificate, Key> pair = keys.entrySet().iterator().next();
|
|
|
return new CAInfo((X509Certificate) pair.getKey(), (PrivateKey) pair.getValue());
|
|
@@ -358,7 +360,7 @@ public class CertificateTool extends LoggingAwareMultiCommand {
|
|
|
Certificate[] certificates = CertParsingUtils.readCertificates(Collections.singletonList(resolvedCaCertPath), env);
|
|
|
if (certificates.length != 1) {
|
|
|
throw new IllegalArgumentException("expected a single certificate in file [" + resolvedCaCertPath + "] but found [" +
|
|
|
- certificates.length + "]");
|
|
|
+ certificates.length + "]");
|
|
|
}
|
|
|
X509Certificate caCert = (X509Certificate) certificates[0];
|
|
|
PrivateKey privateKey = readPrivateKey(key, getChars(password), terminal);
|
|
@@ -391,7 +393,7 @@ public class CertificateTool extends LoggingAwareMultiCommand {
|
|
|
* @return a {@link Collection} of {@link CertificateInformation} that represents each instance
|
|
|
*/
|
|
|
Collection<CertificateInformation> getCertificateInformationList(Terminal terminal, OptionSet options)
|
|
|
- throws Exception {
|
|
|
+ throws Exception {
|
|
|
final Path input = resolvePath(options, inputFileSpec);
|
|
|
if (input != null) {
|
|
|
return parseAndValidateFile(terminal, input.toAbsolutePath());
|
|
@@ -456,7 +458,7 @@ public class CertificateTool extends LoggingAwareMultiCommand {
|
|
|
}
|
|
|
|
|
|
String exit = terminal.readText("Would you like to specify another instance? Press 'y' to continue entering instance " +
|
|
|
- "information: ");
|
|
|
+ "information: ");
|
|
|
if ("y".equals(exit) == false) {
|
|
|
done = true;
|
|
|
}
|
|
@@ -468,7 +470,7 @@ public class CertificateTool extends LoggingAwareMultiCommand {
|
|
|
final boolean isNameValidFilename = Name.isValidFilename(certName);
|
|
|
while (true) {
|
|
|
String filename = terminal.readText("Enter name for directories and files of " + certName +
|
|
|
- (isNameValidFilename ? " [" + certName + "]" : "") + ": ");
|
|
|
+ (isNameValidFilename ? " [" + certName + "]" : "") + ": ");
|
|
|
if (filename.isEmpty() && isNameValidFilename) {
|
|
|
return certName;
|
|
|
}
|
|
@@ -490,7 +492,7 @@ public class CertificateTool extends LoggingAwareMultiCommand {
|
|
|
* @param includeKey if true, write the CA key in PEM format
|
|
|
*/
|
|
|
static void writeCAInfo(ZipOutputStream outputStream, JcaPEMWriter pemWriter, CAInfo info, boolean includeKey)
|
|
|
- throws Exception {
|
|
|
+ throws Exception {
|
|
|
final String caDirName = createCaDirectory(outputStream);
|
|
|
outputStream.putNextEntry(new ZipEntry(caDirName + "ca.crt"));
|
|
|
pemWriter.writeObject(info.certAndKey.cert);
|
|
@@ -546,7 +548,7 @@ public class CertificateTool extends LoggingAwareMultiCommand {
|
|
|
pkcs12.load(null);
|
|
|
withPassword(fileName, password, terminal, p12Password -> {
|
|
|
if (isAscii(p12Password)) {
|
|
|
- pkcs12.setKeyEntry(alias, pair.key, p12Password, new Certificate[] { pair.cert });
|
|
|
+ pkcs12.setKeyEntry(alias, pair.key, p12Password, new Certificate[]{pair.cert});
|
|
|
if (caCert != null) {
|
|
|
pkcs12.setCertificateEntry("ca", caCert);
|
|
|
}
|
|
@@ -574,7 +576,7 @@ public class CertificateTool extends LoggingAwareMultiCommand {
|
|
|
terminal.println("The 'csr' mode generates certificate signing requests that can be sent to");
|
|
|
terminal.println("a trusted certificate authority");
|
|
|
terminal.println(" * By default, this generates a single CSR for a single instance.");
|
|
|
- terminal.println(" * You can use the '-multiple' option to generate CSRs for multiple" );
|
|
|
+ terminal.println(" * You can use the '-multiple' option to generate CSRs for multiple");
|
|
|
terminal.println(" instances, each with their own private key.");
|
|
|
terminal.println(" * The '-in' option allows for the CSR generation to be automated");
|
|
|
terminal.println(" by describing the details of each instance in a YAML file");
|
|
@@ -616,7 +618,7 @@ public class CertificateTool extends LoggingAwareMultiCommand {
|
|
|
for (CertificateInformation certificateInformation : certInfo) {
|
|
|
KeyPair keyPair = CertGenUtils.generateKeyPair(keySize);
|
|
|
GeneralNames sanList = getSubjectAlternativeNamesValue(certificateInformation.ipAddresses,
|
|
|
- certificateInformation.dnsNames, certificateInformation.commonNames);
|
|
|
+ certificateInformation.dnsNames, certificateInformation.commonNames);
|
|
|
PKCS10CertificationRequest csr = CertGenUtils.generateCSR(keyPair, certificateInformation.name.x500Principal, sanList);
|
|
|
|
|
|
final String dirName = certificateInformation.name.filename + "/";
|
|
@@ -750,7 +752,7 @@ public class CertificateTool extends LoggingAwareMultiCommand {
|
|
|
*/
|
|
|
void generateAndWriteSignedCertificates(Path output, boolean writeZipFile, OptionSet options,
|
|
|
Collection<CertificateInformation> certs, CAInfo caInfo, Terminal terminal)
|
|
|
- throws Exception {
|
|
|
+ throws Exception {
|
|
|
|
|
|
checkDirectory(output, terminal);
|
|
|
|
|
@@ -805,7 +807,7 @@ public class CertificateTool extends LoggingAwareMultiCommand {
|
|
|
final String fileName = entryBase + ".p12";
|
|
|
outputStream.putNextEntry(new ZipEntry(fileName));
|
|
|
writePkcs12(fileName, outputStream, certificateInformation.name.originalName, pair, caInfo.certAndKey.cert,
|
|
|
- outputPassword, terminal);
|
|
|
+ outputPassword, terminal);
|
|
|
outputStream.closeEntry();
|
|
|
}
|
|
|
}
|
|
@@ -815,7 +817,7 @@ public class CertificateTool extends LoggingAwareMultiCommand {
|
|
|
CertificateInformation certificateInformation = certs.iterator().next();
|
|
|
CertificateAndKey pair = generateCertificateAndKey(certificateInformation, caInfo, keySize, days);
|
|
|
fullyWriteFile(output, stream -> writePkcs12(output.getFileName().toString(), stream,
|
|
|
- certificateInformation.name.originalName, pair, caInfo.certAndKey.cert, outputPassword, terminal));
|
|
|
+ certificateInformation.name.originalName, pair, caInfo.certAndKey.cert, outputPassword, terminal));
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -823,9 +825,9 @@ public class CertificateTool extends LoggingAwareMultiCommand {
|
|
|
int keySize, int days) throws Exception {
|
|
|
KeyPair keyPair = CertGenUtils.generateKeyPair(keySize);
|
|
|
Certificate certificate = CertGenUtils.generateSignedCertificate(certificateInformation.name.x500Principal,
|
|
|
- getSubjectAlternativeNamesValue(certificateInformation.ipAddresses, certificateInformation.dnsNames,
|
|
|
- certificateInformation.commonNames),
|
|
|
- keyPair, caInfo.certAndKey.cert, caInfo.certAndKey.key, days);
|
|
|
+ getSubjectAlternativeNamesValue(certificateInformation.ipAddresses, certificateInformation.dnsNames,
|
|
|
+ certificateInformation.commonNames),
|
|
|
+ keyPair, caInfo.certAndKey.cert, caInfo.certAndKey.key, days);
|
|
|
return new CertificateAndKey((X509Certificate) certificate, keyPair.getPrivate());
|
|
|
}
|
|
|
|
|
@@ -872,7 +874,7 @@ public class CertificateTool extends LoggingAwareMultiCommand {
|
|
|
} else {
|
|
|
final String fileName = output.getFileName().toString();
|
|
|
fullyWriteFile(output, outputStream ->
|
|
|
- writePkcs12(fileName, outputStream, "ca", caInfo.certAndKey, null, caInfo.password, terminal));
|
|
|
+ writePkcs12(fileName, outputStream, "ca", caInfo.certAndKey, null, caInfo.password, terminal));
|
|
|
}
|
|
|
}
|
|
|
}
|
|
@@ -912,7 +914,7 @@ public class CertificateTool extends LoggingAwareMultiCommand {
|
|
|
try (Reader reader = Files.newBufferedReader(file)) {
|
|
|
// EMPTY is safe here because we never use namedObject
|
|
|
XContentParser xContentParser = XContentType.YAML.xContent()
|
|
|
- .createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, reader);
|
|
|
+ .createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, reader);
|
|
|
return CertificateToolParser.PARSER.parse(xContentParser, new ArrayList<>(), null);
|
|
|
}
|
|
|
}
|
|
@@ -1015,7 +1017,7 @@ public class CertificateTool extends LoggingAwareMultiCommand {
|
|
|
* @return the {@link PrivateKey} that was read from the file
|
|
|
*/
|
|
|
private static PrivateKey readPrivateKey(Path path, char[] password, Terminal terminal)
|
|
|
- throws Exception {
|
|
|
+ throws Exception {
|
|
|
AtomicReference<char[]> passwordReference = new AtomicReference<>(password);
|
|
|
try {
|
|
|
return PemUtils.readPrivateKey(path, () -> {
|
|
@@ -1125,7 +1127,7 @@ public class CertificateTool extends LoggingAwareMultiCommand {
|
|
|
}
|
|
|
} catch (IllegalArgumentException e) {
|
|
|
String error = "[" + name + "] could not be converted to a valid DN\n" + e.getMessage() + "\n"
|
|
|
- + ExceptionsHelper.stackTrace(e);
|
|
|
+ + ExceptionsHelper.stackTrace(e);
|
|
|
return new Name(name, null, null, error);
|
|
|
}
|
|
|
|
|
@@ -1138,15 +1140,15 @@ public class CertificateTool extends LoggingAwareMultiCommand {
|
|
|
|
|
|
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;
|
|
|
+ && 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 + "]}";
|
|
|
+ + "{original=[" + originalName + "] principal=[" + x500Principal
|
|
|
+ + "] file=[" + filename + "] err=[" + error + "]}";
|
|
|
}
|
|
|
}
|
|
|
|