Add certutil http command (#50952)

This adds a new "http" sub-command to the certutil CLI tool.

The http command generates certificates/CSRs for use on the http
interface of an elasticsearch node/cluster.
It is designed to be a guided tool that provides explanations and
sugestions for each of the configuration options. The generated zip
file output includes extensive "readme" documentation and sample
configuration files for core Elastic products.

Backport of: #49827
This commit is contained in:
Tim Vernum 2020-01-14 21:24:21 +11:00 committed by GitHub
parent 22ba759e1f
commit 2bb7b53e41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 2494 additions and 5 deletions

View File

@ -130,8 +130,20 @@ public class CertParsingUtils {
* return the password for that key. If it returns {@code null}, then the key-pair for that alias is not read.
*/
public static Map<Certificate, Key> readPkcs12KeyPairs(Path path, char[] password, Function<String, char[]> keyPassword)
throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, UnrecoverableKeyException {
final KeyStore store = readKeyStore(path, "PKCS12", password);
throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, UnrecoverableKeyException {
return readKeyPairsFromKeystore(path, "PKCS12", password, keyPassword);
}
public static Map<Certificate, Key> readKeyPairsFromKeystore(Path path, String storeType, char[] password,
Function<String, char[]> keyPassword)
throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException, UnrecoverableKeyException {
final KeyStore store = readKeyStore(path, storeType, password);
return readKeyPairsFromKeystore(store, keyPassword);
}
static Map<Certificate, Key> readKeyPairsFromKeystore(KeyStore store, Function<String, char[]> keyPassword)
throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
final Enumeration<String> enumeration = store.aliases();
final Map<Certificate, Key> map = new HashMap<>(store.size());
while (enumeration.hasMoreElements()) {

View File

@ -0,0 +1,90 @@
/*
* 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.test;
import org.hamcrest.CustomMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
public class FileMatchers {
public static Matcher<Path> pathExists(LinkOption... options) {
return new CustomMatcher<Path>("Path exists") {
@Override
public boolean matches(Object item) {
if (item instanceof Path) {
Path path = (Path) item;
return Files.exists(path, options);
} else {
return false;
}
}
};
}
public static Matcher<Path> isDirectory(LinkOption... options) {
return new FileTypeMatcher("directory", options) {
@Override
protected boolean matchPath(Path path) {
return Files.isDirectory(path, options);
}
};
}
public static Matcher<Path> isRegularFile(LinkOption... options) {
return new FileTypeMatcher("regular file", options) {
@Override
protected boolean matchPath(Path path) {
return Files.isRegularFile(path, options);
}
};
}
private abstract static class FileTypeMatcher extends CustomMatcher<Path> {
private final LinkOption[] options;
FileTypeMatcher(String typeName, LinkOption... options) {
super("Path is " + typeName);
this.options = options;
}
@Override
public boolean matches(Object item) {
if (item instanceof Path) {
Path path = (Path) item;
return matchPath(path);
} else {
return false;
}
}
protected abstract boolean matchPath(Path path);
@Override
public void describeMismatch(Object item, Description description) {
super.describeMismatch(item, description);
if (item instanceof Path) {
Path path = (Path) item;
if (Files.exists(path, options) == false) {
description.appendText(" (file not found)");
} else if (Files.isDirectory(path, options)) {
description.appendText(" (directory)");
} else if (Files.isSymbolicLink(path)) {
description.appendText(" (symlink)");
} else if (Files.isRegularFile(path, options)) {
description.appendText(" (regular file)");
} else {
description.appendText(" (unknown file type)");
}
}
}
}
}

View File

@ -20,6 +20,10 @@ import java.util.regex.Pattern;
public class TestMatchers extends Matchers {
/**
* @deprecated Use {@link FileMatchers#pathExists}
*/
@Deprecated
public static Matcher<Path> pathExists(Path path, LinkOption... options) {
return new CustomMatcher<Path>("Path " + path + " exists") {
@Override

View File

@ -19,6 +19,11 @@ dependencyLicenses {
mapping from: /bc.*/, to: 'bouncycastle'
}
forbiddenPatterns {
exclude '**/*.p12'
exclude '**/*.jks'
}
rootProject.globalInfo.ready {
if (BuildParams.inFipsJvm) {
test.enabled = false

View File

@ -157,6 +157,14 @@ public class CertGenUtils {
throw new IllegalArgumentException("the certificate must be valid for at least one day");
}
final ZonedDateTime notAfter = notBefore.plusDays(days);
return generateSignedCertificate(principal, subjectAltNames, keyPair, caCert, caPrivKey, isCa, notBefore, notAfter,
signatureAlgorithm);
}
public static X509Certificate generateSignedCertificate(X500Principal principal, GeneralNames subjectAltNames, KeyPair keyPair,
X509Certificate caCert, PrivateKey caPrivKey, boolean isCa,
ZonedDateTime notBefore, ZonedDateTime notAfter, String signatureAlgorithm)
throws NoSuchAlgorithmException, CertIOException, OperatorCreationException, CertificateException {
final BigInteger serial = CertGenUtils.getSerial();
JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils();

View File

@ -142,6 +142,7 @@ public class CertificateTool extends LoggingAwareMultiCommand {
subcommands.put("csr", new SigningRequestCommand());
subcommands.put("cert", new GenerateCertificateCommand());
subcommands.put("ca", new CertificateAuthorityCommand());
subcommands.put("http", new HttpCertificateCommand());
}
@ -920,7 +921,7 @@ public class CertificateTool extends LoggingAwareMultiCommand {
}
}
private static PEMEncryptor getEncrypter(char[] password) {
static PEMEncryptor getEncrypter(char[] password) {
return new JcePEMEncryptorBuilder("DES-EDE3-CBC").setProvider(BC_PROV).build(password);
}
@ -1036,7 +1037,7 @@ public class CertificateTool extends LoggingAwareMultiCommand {
}
}
private static GeneralNames getSubjectAlternativeNamesValue(List<String> ipAddresses, List<String> dnsNames, List<String> commonNames) {
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));
@ -1056,7 +1057,7 @@ public class CertificateTool extends LoggingAwareMultiCommand {
return new GeneralNames(generalNameList.toArray(new GeneralName[0]));
}
private static boolean isAscii(char[] str) {
static boolean isAscii(char[] str) {
return ASCII_ENCODER.canEncode(CharBuffer.wrap(str));
}

View File

@ -0,0 +1,33 @@
There are two files in this directory:
1. This README file
2. ${P12}
## ${P12}
The "${P12}" file is a PKCS#12 format keystore.
It contains a copy of the certificate and private key for your Certificate Authority.
You should keep this file secure, and should not provide it to anyone else.
The sole purpose for this keystore is to generate new certificates if you add additional nodes to your Elasticsearch cluster, or need to
update the server names (hostnames or IP addresses) of your nodes.
This keystore is not required in order to operate any Elastic product or client.
We recommended that you keep the file somewhere safe, and do not deploy it to your production servers.
#if PASSWORD
Your keystore is protected by a password.
Your password has not been stored anywhere - it is your responsibility to keep it safe.
#else
Your keystore has a blank password.
It is important that you protect this file - if someone else gains access to your private key they can impersonate your Elasticsearch node.
#endif
If you wish to create additional certificates for the nodes in your cluster you can provide this keystore to the "elasticsearch-certutil"
utility as shown in the example below:
elasticsearch-certutil cert --ca ${P12} --dns "hostname.of.your.node" --pass
See the elasticsearch-certutil documentation for additional options.

View File

@ -0,0 +1,56 @@
There are four files in this directory:
1. This README file
2. ${CSR}
3. ${KEY}
4. ${YML}
## ${CSR}
The "${CSR}" file is a Certificate Signing Request.
You should provide a copy this file to a Certificate Authority ("CA"), and they will provide you with a signed Certificate.
In many large organisations there is a central security team that operates an internal Certificate Authority that can generate your
certificate for you. Alternatively, it may be possible to have a your certificate generated by a commercial Certificate Authority.
In either case, you need to provide the ${CSR} file to the certificate authority, and they will provide you with your signed certificate.
For the purposes of this document, we assume that when they send you your certificate, you will save it as a file named "${CERT}".
The certificate authority might also provide you with a copy of their signing certificate. If they do, you should keep a copy of that
certificate, as you may need it when configuring clients such as Kibana.
## ${KEY}
The "${KEY}" file is your private key.
You should keep this file secure, and should not provide it to anyone else (not even the CA).
Once you have a copy of your certificate (from the CA), you will configure your Elasticsearch nodes to use the certificate
and this private key.
You will need to copy both of those files to your elasticsearch configuration directory.
#if PASSWORD
Your private key is protected by a passphrase.
Your password has not been stored anywhere - it is your responsibility to keep it safe.
When you configure elasticsearch to enable SSL (but not before then), you will need to provide the key's password as a secure
configuration setting in Elasticsearch so that it can decrypt your private key.
The command for this is:
elasticsearch-keystore add "xpack.security.http.ssl.secure_key_passphrase"
#else
Your private key is not password protected.
It is important that you protect this file - if someone else gains access to your private key they can impersonate your Elasticsearch node.
#endif
## ${YML}
This is a sample configuration for Elasticsearch to enable SSL on the http interface.
You can use this sample to update the "elasticsearch.yml" configuration file in your config directory.
The location of this directory can vary depending on how you installed Elasticsearch, but based on your system it appears that your config
directory is ${CONF_DIR}
You will not be able to configure Elasticsearch until the Certificate Authority processes your CSR and provides you with a copy of your
certificate. When you have a copy of the certificate you should copy it and the private key ("${KEY}") to the config directory.
The sample config assumes that the certificate is named "${CERT}".

View File

@ -0,0 +1,38 @@
There are three files in this directory:
1. This README file
2. ${P12}
3. ${YML}
## ${P12}
The "${P12}" file is a PKCS#12 format keystore.
It contains a copy of your certificate and the associated private key.
You should keep this file secure, and should not provide it to anyone else.
You will need to copy this file to your elasticsearch configuration directory.
#if PASSWORD
Your keystore is protected by a password.
Your password has not been stored anywhere - it is your responsibility to keep it safe.
When you configure elasticsearch to enable SSL (but not before then), you will need to provide the keystore's password as a secure
configuration setting in Elasticsearch so that it can access your private key.
The command for this is:
elasticsearch-keystore add "xpack.security.http.ssl.keystore.secure_password"
#else
Your keystore has a blank password.
It is important that you protect this file - if someone else gains access to your private key they can impersonate your Elasticsearch node.
#endif
## ${YML}
This is a sample configuration for Elasticsearch to enable SSL on the http interface.
You can use this sample to update the "elasticsearch.yml" configuration file in your config directory.
The location of this directory can vary depending on how you installed Elasticsearch, but based on your system it appears that your config
directory is ${CONF_DIR}
This sample configuration assumes that you have copied your ${P12} file directly into the config directory without renaming it.

View File

@ -0,0 +1,32 @@
#
# SAMPLE ELASTICSEARCH CONFIGURATION FOR ENABLING SSL ON THE HTTP INTERFACE
#
# This is a sample configuration snippet for Elasticsearch that enables and configures SSL for the HTTP (Rest) interface
#
# This was automatically generated at: ${DATE} ${TIME}
# This configuration was intended for Elasticsearch version ${VERSION}
#
# You should review these settings, and then update the main configuration file at
# ${CONF_DIR}/elasticsearch.yml
#
# This turns on SSL for the HTTP (Rest) interface
xpack.security.http.ssl.enabled: true
# This configures the certificate to use.
# This certificate will be generated by your Certificate Authority, based on the CSR that you sent to them.
xpack.security.http.ssl.certificate: "${CERT}"
# This configures the private key for your certificate.
#if PASSWORD
# Because your private key is encrypted, you will also need to add the passphrase to the Elasticsearch keystore
# elasticsearch-keystore add "xpack.security.http.ssl.secure_key_passphrase"
#endif
xpack.security.http.ssl.key: "${KEY}"
# If your Certificate Authorities provides you with a copy of their certificate you can configure it here.
# This is not strictly necessary, but can make it easier when running other elasticsearch utilities such as the "setup-passwords" tool.
#
#xpack.security.http.ssl.certificate_authorities: [ "ca.crt" ]
#

View File

@ -0,0 +1,22 @@
#
# SAMPLE ELASTICSEARCH CONFIGURATION FOR ENABLING SSL ON THE HTTP INTERFACE
#
# This is a sample configuration snippet for Elasticsearch that enables and configures SSL for the HTTP (Rest) interface
#
# This was automatically generated at: ${DATE} ${TIME}
# This configuration was intended for Elasticsearch version ${VERSION}
#
# You should review these settings, and then update the main configuration file at
# ${CONF_DIR}/elasticsearch.yml
#
# This turns on SSL for the HTTP (Rest) interface
xpack.security.http.ssl.enabled: true
# This configures the keystore to use for SSL on HTTP
#if PASSWORD
# Because your keystore has a password, you will also need to add the password to the Elasticsearch keystore
# elasticsearch-keystore add "xpack.security.http.ssl.keystore.secure_password"
#endif
xpack.security.http.ssl.keystore.path: "${P12}"

View File

@ -0,0 +1,62 @@
#if CA_CERT
There are three files in this directory:
1. This README file
2. ${CA_CERT}
3. ${YML}
#else
There are two files in this directory:
1. This README file
2. ${YML}
#endif
#if CA_CERT
## ${CA_CERT}
The "${CA_CERT}" file is a PEM format X.509 Certificate for the Elasticsearch Certificate Authority.
You need to configure Kibana to trust this certificate as an issuing CA for TLS connections to your Elasticsearch cluster.
The "${YML}" file, and the instructions below, explain how to do this.
#else
Because your Elasticsearch certificates are being generated by an external CA (via a Certificate Signing Request), this directory does not
contain a copy of the CA's issuing certificate (we don't know where you will send your CSRs and who will sign them).
If you are using a public (commercial) CA then it is likely that Kibana will already be configured to trust this CA and you will not need
to do any special configuration.
However, if you are using a CA that is specific to your organisation, then you will need to configure Kibana to trust that CA.
When your CA issues your certificate, you should ask them for a copy of their certificate chain in PEM format.
The "${YML}" file, and the instructions below, explain what to do this with this file.
#endif
## ${YML}
This is a sample configuration for Kibana to enable SSL for connections to Elasticsearch.
You can use this sample to update the "kibana.yml" configuration file in your Kibana config directory.
-------------------------------------------------------------------------------------------------
NOTE:
You also need to update the URLs in your "elasticsearch.hosts" setting to use the "https" URL.
e.g. If your kibana.yml file currently has
elasticsearch.hosts: [ "http://localhost:9200" ]
then you should change this to:
elasticsearch.hosts: [ "https://localhost:9200" ]
-------------------------------------------------------------------------------------------------
#if CA_CERT
The sample configuration assumes that you have copied the "${CA_CERT}" file directly into the Kibana config
directory without renaming it.
#else
The sample configuration assumes that you have a file named "${CA_CERT_NAME}" which contains your CA's certificate
chain, and have copied that file into the Kibana config directory.
#endif

View File

@ -0,0 +1,25 @@
#
# SAMPLE KIBANA CONFIGURATION FOR ENABLING SSL TO ELASTICSEARCH
#
# This is a sample configuration snippet for Kibana that configures SSL for connections to Elasticsearch
#
# This was automatically generated at: ${DATE} ${TIME}
# This configuration was intended for version ${VERSION}
#
# You should review these settings, and then update the main kibana.yml configuration file.
#
#-------------------------------------------------------------------------------------------------
# You also need to update the URLs in your "elasticsearch.hosts" setting to use the "https" URL.
# e.g. If your kibana.yml file currently has
#
# elasticsearch.hosts: [ "http://localhost:9200" ]
#
# then you should change this to:
#
# elasticsearch.hosts: [ "https://localhost:9200" ]
#
#-------------------------------------------------------------------------------------------------
# This configures Kibana to trust a specific Certificate Authority for connections to Elasticsearch
elasticsearch.ssl.certificateAuthorities: [ "config/${CA_CERT_NAME}" ]

View File

@ -0,0 +1,792 @@
/*
* 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.security.cli;
import com.google.common.jimfs.Configuration;
import com.google.common.jimfs.Jimfs;
import joptsimple.OptionSet;
import org.bouncycastle.asn1.DERIA5String;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.DERSequence;
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.pkcs.PKCS10CertificationRequest;
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader;
import org.elasticsearch.cli.MockTerminal;
import org.elasticsearch.common.CheckedBiFunction;
import org.elasticsearch.common.CheckedFunction;
import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.common.network.NetworkAddress;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.core.internal.io.IOUtils;
import org.elasticsearch.env.Environment;
import org.elasticsearch.env.TestEnvironment;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xpack.core.ssl.CertParsingUtils;
import org.elasticsearch.xpack.core.ssl.PemUtils;
import org.elasticsearch.xpack.security.cli.HttpCertificateCommand.FileType;
import org.hamcrest.Matchers;
import org.junit.Before;
import org.junit.BeforeClass;
import javax.security.auth.x500.X500Principal;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.InetAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
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.security.GeneralSecurityException;
import java.security.Key;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAKey;
import java.time.Instant;
import java.time.Period;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.util.Collections.singletonMap;
import static org.elasticsearch.test.FileMatchers.isDirectory;
import static org.elasticsearch.test.FileMatchers.isRegularFile;
import static org.elasticsearch.test.FileMatchers.pathExists;
import static org.elasticsearch.xpack.security.cli.HttpCertificateCommand.guessFileType;
import static org.hamcrest.Matchers.arrayWithSize;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.in;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.oneOf;
public class HttpCertificateCommandTests extends ESTestCase {
private static final String CA_PASSWORD = "ca-password";
private FileSystem jimfs;
private Path testRoot;
@Before
public void createTestDir() throws Exception {
Configuration conf = Configuration.unix().toBuilder().setAttributeViews("posix").build();
jimfs = Jimfs.newFileSystem(conf);
testRoot = jimfs.getPath(getClass().getSimpleName() + "-" + getTestName());
IOUtils.rm(testRoot);
Files.createDirectories(testRoot);
}
@BeforeClass
public static void muteInFips() {
assumeFalse("Can't run in a FIPS JVM", inFipsJvm());
}
public void testGenerateSingleCertificateSigningRequest() throws Exception {
final Path outFile = testRoot.resolve("csr.zip").toAbsolutePath();
final List<String> hostNames = randomHostNames();
final List<String> ipAddresses = randomIpAddresses();
final String certificateName = hostNames.get(0);
final HttpCertificateCommand command = new PathAwareHttpCertificateCommand(outFile);
final MockTerminal terminal = new MockTerminal();
terminal.addTextInput("y"); // generate CSR
terminal.addTextInput(randomBoolean() ? "n" : ""); // cert-per-node
// enter hostnames
hostNames.forEach(terminal::addTextInput);
terminal.addTextInput(""); // end-of-hosts
terminal.addTextInput(randomBoolean() ? "y" : ""); // yes, correct
// enter ip names
ipAddresses.forEach(terminal::addTextInput);
terminal.addTextInput(""); // end-of-ips
terminal.addTextInput(randomBoolean() ? "y" : ""); // yes, correct
terminal.addTextInput(randomBoolean() ? "n" : ""); // don't change advanced settings
final String password = randomPassword();
terminal.addSecretInput(password);
terminal.addSecretInput(password); // confirm
terminal.addTextInput(outFile.toString());
final Environment env = newEnvironment();
final OptionSet options = command.getParser().parse(new String[0]);
command.execute(terminal, options, env);
Path zipRoot = getZipRoot(outFile);
assertThat(zipRoot.resolve("elasticsearch"), isDirectory());
final Path csrPath = zipRoot.resolve("elasticsearch/http-" + certificateName + ".csr");
final PKCS10CertificationRequest csr = readPemObject(csrPath, "CERTIFICATE REQUEST", PKCS10CertificationRequest::new);
final Path keyPath = zipRoot.resolve("elasticsearch/http-" + certificateName + ".key");
final AtomicBoolean wasEncrypted = new AtomicBoolean(false);
final PrivateKey privateKey = PemUtils.readPrivateKey(keyPath, () -> {
wasEncrypted.set(true);
return password.toCharArray();
});
assertTrue("Password should have been required to decrypted key", wasEncrypted.get());
final Path esReadmePath = zipRoot.resolve("elasticsearch/README.txt");
assertThat(esReadmePath, isRegularFile());
final String esReadme = new String(Files.readAllBytes(esReadmePath), StandardCharsets.UTF_8);
final Path ymlPath = zipRoot.resolve("elasticsearch/sample-elasticsearch.yml");
assertThat(ymlPath, isRegularFile());
final String yml = new String(Files.readAllBytes(ymlPath), StandardCharsets.UTF_8);
// Verify the CSR was built correctly
verifyCertificationRequest(csr, certificateName, hostNames, ipAddresses);
// Verify the key
assertMatchingPair(getPublicKey(csr), privateKey);
final String crtName = keyPath.getFileName().toString().replace(".csr", ".crt");
// Verify the README
assertThat(esReadme, containsString(csrPath.getFileName().toString()));
assertThat(esReadme, containsString(crtName));
assertThat(esReadme, containsString(keyPath.getFileName().toString()));
assertThat(esReadme, containsString(ymlPath.getFileName().toString()));
assertThat(esReadme, not(containsString(password)));
// Verify the yml
assertThat(yml, not(containsString(csrPath.getFileName().toString())));
assertThat(yml, containsString(crtName));
assertThat(yml, containsString(keyPath.getFileName().toString()));
assertThat(yml, not(containsString(password)));
// Should not be a CA directory in CSR mode
assertThat(zipRoot.resolve("ca"), not(pathExists()));
// No CA in CSR mode
verifyKibanaDirectory(zipRoot,
false,
Collections.singletonList("Certificate Signing Request"),
Arrays.asList(password, csrPath.getFileName().toString()));
}
public void testGenerateSingleCertificateWithExistingCA() throws Exception {
final Path outFile = testRoot.resolve("certs.zip").toAbsolutePath();
final List<String> hostNames = randomHostNames();
final List<String> ipAddresses = randomIpAddresses();
final String certificateName = hostNames.get(0);
final Path caCertPath = getDataPath("ca.crt");
assertThat(caCertPath, isRegularFile());
final Path caKeyPath = getDataPath("ca.key");
assertThat(caKeyPath, isRegularFile());
final String caPassword = CA_PASSWORD;
final int years = randomIntBetween(1, 8);
final HttpCertificateCommand command = new PathAwareHttpCertificateCommand(outFile);
final MockTerminal terminal = new MockTerminal();
terminal.addTextInput(randomBoolean() ? "n" : ""); // don't generate CSR
terminal.addTextInput("y"); // existing CA
// randomise between cert+key, key+cert, PKCS12 : the tool is smart enough to handle any of those.
switch (randomFrom(FileType.PEM_CERT, FileType.PEM_KEY, FileType.PKCS12)) {
case PEM_CERT:
terminal.addTextInput(caCertPath.toAbsolutePath().toString());
terminal.addTextInput(caKeyPath.toAbsolutePath().toString());
break;
case PEM_KEY:
terminal.addTextInput(caKeyPath.toAbsolutePath().toString());
terminal.addTextInput(caCertPath.toAbsolutePath().toString());
break;
case PKCS12:
terminal.addTextInput(getDataPath("ca.p12").toAbsolutePath().toString());
break;
}
terminal.addSecretInput(caPassword);
terminal.addTextInput(years + "y"); // validity period
terminal.addTextInput(randomBoolean() ? "n" : ""); // don't use cert-per-node
// enter hostnames
hostNames.forEach(terminal::addTextInput);
terminal.addTextInput(""); // end-of-hosts
terminal.addTextInput(randomBoolean() ? "y" : ""); // yes, correct
// enter ip names
ipAddresses.forEach(terminal::addTextInput);
terminal.addTextInput(""); // end-of-ips
terminal.addTextInput(randomBoolean() ? "y" : ""); // yes, correct
terminal.addTextInput(randomBoolean() ? "n" : ""); // don't change advanced settings
final String password = randomPassword();
terminal.addSecretInput(password);
terminal.addSecretInput(password); // confirm
terminal.addTextInput(outFile.toString());
final Environment env = newEnvironment();
final OptionSet options = command.getParser().parse(new String[0]);
command.execute(terminal, options, env);
Path zipRoot = getZipRoot(outFile);
assertThat(zipRoot.resolve("elasticsearch"), isDirectory());
final Path p12Path = zipRoot.resolve("elasticsearch/http.p12");
final Path readmePath = zipRoot.resolve("elasticsearch/README.txt");
assertThat(readmePath, isRegularFile());
final String readme = new String(Files.readAllBytes(readmePath), StandardCharsets.UTF_8);
final Path ymlPath = zipRoot.resolve("elasticsearch/sample-elasticsearch.yml");
assertThat(ymlPath, isRegularFile());
final String yml = new String(Files.readAllBytes(ymlPath), StandardCharsets.UTF_8);
final Tuple<X509Certificate, PrivateKey> certAndKey = readCertificateAndKey(p12Path, password.toCharArray());
// Verify the Cert was built correctly
verifyCertificate(certAndKey.v1(), certificateName, years, hostNames, ipAddresses);
assertThat(getRSAKeySize(certAndKey.v1().getPublicKey()), is(HttpCertificateCommand.DEFAULT_CERT_KEY_SIZE));
assertThat(getRSAKeySize(certAndKey.v2()), is(HttpCertificateCommand.DEFAULT_CERT_KEY_SIZE));
final X509Certificate caCert = readPemCertificate(caCertPath);
verifyChain(certAndKey.v1(), caCert);
// Verify the README
assertThat(readme, containsString(p12Path.getFileName().toString()));
assertThat(readme, containsString(ymlPath.getFileName().toString()));
assertThat(readme, not(containsString(password)));
assertThat(readme, not(containsString(caPassword)));
// Verify the yml
assertThat(yml, containsString(p12Path.getFileName().toString()));
assertThat(yml, not(containsString(password)));
assertThat(yml, not(containsString(caPassword)));
// Should not be a CA directory when using an existing CA.
assertThat(zipRoot.resolve("ca"), not(pathExists()));
verifyKibanaDirectory(zipRoot, true, Collections.singletonList("2. elasticsearch-ca.pem"),
Arrays.asList(password, caPassword, caKeyPath.getFileName().toString()));
}
public void testGenerateMultipleCertificateWithNewCA() throws Exception {
final Path outFile = testRoot.resolve("certs.zip").toAbsolutePath();
final int numberCerts = randomIntBetween(3, 6);
final String[] certNames = new String[numberCerts];
final String[] hostNames = new String[numberCerts];
for (int i = 0; i < numberCerts; i++) {
certNames[i] = randomAlphaOfLengthBetween(6, 12);
hostNames[i] = randomAlphaOfLengthBetween(4, 8);
}
final HttpCertificateCommand command = new PathAwareHttpCertificateCommand(outFile);
final MockTerminal terminal = new MockTerminal();
terminal.addTextInput(randomBoolean() ? "n" : ""); // don't generate CSR
terminal.addTextInput(randomBoolean() ? "n" : ""); // no existing CA
final String caDN;
final int caYears;
final int caKeySize;
// randomise whether to change CA defaults.
if (randomBoolean()) {
terminal.addTextInput("y"); // Change defaults
caDN = "CN=" + randomAlphaOfLengthBetween(3, 8);
caYears = randomIntBetween(1, 3);
caKeySize = randomFrom(2048, 3072, 4096);
terminal.addTextInput(caDN);
terminal.addTextInput(caYears + "y");
terminal.addTextInput(Integer.toString(caKeySize));
terminal.addTextInput("n"); // Don't change values
} else {
terminal.addTextInput(randomBoolean() ? "n" : ""); // Don't change defaults
caDN = HttpCertificateCommand.DEFAULT_CA_NAME.toString();
caYears = HttpCertificateCommand.DEFAULT_CA_VALIDITY.getYears();
caKeySize = HttpCertificateCommand.DEFAULT_CA_KEY_SIZE;
}
final String caPassword = randomPassword();
terminal.addSecretInput(caPassword);
terminal.addSecretInput(caPassword); // confirm
final int certYears = randomIntBetween(1, 8);
terminal.addTextInput(certYears + "y"); // node cert validity period
terminal.addTextInput("y"); // cert-per-node
for (int i = 0; i < numberCerts; i++) {
if (i != 0) {
terminal.addTextInput(randomBoolean() ? "y" : ""); // another cert
}
// certificate / node name
terminal.addTextInput(certNames[i]);
// enter hostname
terminal.addTextInput(hostNames[i]); // end-of-hosts
terminal.addTextInput(""); // end-of-hosts
terminal.addTextInput(randomBoolean() ? "y" : ""); // yes, correct
// no ip
terminal.addTextInput(""); // end-of-ip
terminal.addTextInput(randomBoolean() ? "y" : ""); // yes, correct
terminal.addTextInput(randomBoolean() ? "n" : ""); // don't change advanced settings
}
terminal.addTextInput("n"); // no more certs
final String password = randomPassword();
terminal.addSecretInput(password);
terminal.addSecretInput(password); // confirm
terminal.addTextInput(outFile.toString());
final Environment env = newEnvironment();
final OptionSet options = command.getParser().parse(new String[0]);
command.execute(terminal, options, env);
Path zipRoot = getZipRoot(outFile);
// Should have a CA directory with the generated CA.
assertThat(zipRoot.resolve("ca"), isDirectory());
final Path caPath = zipRoot.resolve("ca/ca.p12");
final Tuple<X509Certificate, PrivateKey> caCertKey = readCertificateAndKey(caPath, caPassword.toCharArray());
verifyCertificate(caCertKey.v1(), caDN.replaceFirst("CN=", ""), caYears, Collections.emptyList(), Collections.emptyList());
assertThat(getRSAKeySize(caCertKey.v1().getPublicKey()), is(caKeySize));
assertThat(getRSAKeySize(caCertKey.v2()), is(caKeySize));
assertThat(zipRoot.resolve("elasticsearch"), isDirectory());
for (int i = 0; i < numberCerts; i++) {
assertThat(zipRoot.resolve("elasticsearch/" + certNames[i]), isDirectory());
final Path p12Path = zipRoot.resolve("elasticsearch/" + certNames[i] + "/http.p12");
assertThat(p12Path, isRegularFile());
final Path readmePath = zipRoot.resolve("elasticsearch/" + certNames[i] + "/README.txt");
assertThat(readmePath, isRegularFile());
final String readme = new String(Files.readAllBytes(readmePath), StandardCharsets.UTF_8);
final Path ymlPath = zipRoot.resolve("elasticsearch/" + certNames[i] + "/sample-elasticsearch.yml");
assertThat(ymlPath, isRegularFile());
final String yml = new String(Files.readAllBytes(ymlPath), StandardCharsets.UTF_8);
final Tuple<X509Certificate, PrivateKey> certAndKey = readCertificateAndKey(p12Path, password.toCharArray());
// Verify the Cert was built correctly
verifyCertificate(certAndKey.v1(), certNames[i], certYears, Collections.singletonList(hostNames[i]), Collections.emptyList());
verifyChain(certAndKey.v1(), caCertKey.v1());
assertThat(getRSAKeySize(certAndKey.v1().getPublicKey()), is(HttpCertificateCommand.DEFAULT_CERT_KEY_SIZE));
assertThat(getRSAKeySize(certAndKey.v2()), is(HttpCertificateCommand.DEFAULT_CERT_KEY_SIZE));
// Verify the README
assertThat(readme, containsString(p12Path.getFileName().toString()));
assertThat(readme, containsString(ymlPath.getFileName().toString()));
assertThat(readme, not(containsString(password)));
assertThat(readme, not(containsString(caPassword)));
// Verify the yml
assertThat(yml, containsString(p12Path.getFileName().toString()));
assertThat(yml, not(containsString(password)));
assertThat(yml, not(containsString(caPassword)));
}
verifyKibanaDirectory(zipRoot, true, Collections.singletonList("2. elasticsearch-ca.pem"),
Arrays.asList(password, caPassword, caPath.getFileName().toString()));
}
public void testParsingValidityPeriod() throws Exception {
final HttpCertificateCommand command = new HttpCertificateCommand();
final MockTerminal terminal = new MockTerminal();
terminal.addTextInput("2y");
assertThat(command.readPeriodInput(terminal, "", null, 1), is(Period.ofYears(2)));
terminal.addTextInput("18m");
assertThat(command.readPeriodInput(terminal, "", null, 1), is(Period.ofMonths(18)));
terminal.addTextInput("90d");
assertThat(command.readPeriodInput(terminal, "", null, 1), is(Period.ofDays(90)));
terminal.addTextInput("1y, 6m");
assertThat(command.readPeriodInput(terminal, "", null, 1), is(Period.ofYears(1).withMonths(6)));
// Test: Re-prompt on bad input.
terminal.addTextInput("2m & 4d");
terminal.addTextInput("2m 4d");
assertThat(command.readPeriodInput(terminal, "", null, 1), is(Period.ofMonths(2).withDays(4)));
terminal.addTextInput("1y, 6m");
assertThat(command.readPeriodInput(terminal, "", null, 1), is(Period.ofYears(1).withMonths(6)));
// Test: Accept default value
final Period p = Period.of(randomIntBetween(1, 5), randomIntBetween(0, 11), randomIntBetween(0, 30));
terminal.addTextInput("");
assertThat(command.readPeriodInput(terminal, "", p, 1), is(p));
final int y = randomIntBetween(1, 5);
final int m = randomIntBetween(1, 11);
final int d = randomIntBetween(1, 30);
terminal.addTextInput(y + "y " + m + "m " + d + "d");
assertThat(command.readPeriodInput(terminal, "", null, 1), is(Period.of(y, m, d)));
// Test: Minimum Days
final int shortDays = randomIntBetween(1, 20);
terminal.addTextInput(shortDays + "d");
terminal.addTextInput("y"); // I'm sure
assertThat(command.readPeriodInput(terminal, "", null, 21), is(Period.ofDays(shortDays)));
terminal.addTextInput(shortDays + "d");
terminal.addTextInput("n"); // I'm not sure
terminal.addTextInput("30d");
assertThat(command.readPeriodInput(terminal, "", null, 21), is(Period.ofDays(30)));
terminal.addTextInput("2m");
terminal.addTextInput("n"); // I'm not sure
terminal.addTextInput("2y");
assertThat(command.readPeriodInput(terminal, "", null, 90), is(Period.ofYears(2)));
}
public void testValidityPeriodToString() throws Exception {
assertThat(HttpCertificateCommand.toString(Period.ofYears(2)), is("2y"));
assertThat(HttpCertificateCommand.toString(Period.ofMonths(5)), is("5m"));
assertThat(HttpCertificateCommand.toString(Period.ofDays(60)), is("60d"));
assertThat(HttpCertificateCommand.toString(Period.ZERO), is("0d"));
assertThat(HttpCertificateCommand.toString(null), is("N/A"));
final int y = randomIntBetween(1, 5);
final int m = randomIntBetween(1, 11);
final int d = randomIntBetween(1, 30);
assertThat(HttpCertificateCommand.toString(Period.of(y, m, d)), is(y + "y," + m + "m," + d + "d"));
}
public void testGuessFileType() throws Exception {
MockTerminal terminal = new MockTerminal();
final Path caCert = getDataPath("ca.crt");
final Path caKey = getDataPath("ca.key");
assertThat(guessFileType(caCert, terminal), is(FileType.PEM_CERT));
assertThat(guessFileType(caKey, terminal), is(FileType.PEM_KEY));
final Path certChain = testRoot.resolve("ca.pem");
try (OutputStream out = Files.newOutputStream(certChain)) {
Files.copy(getDataPath("testnode.crt"), out);
Files.copy(caCert, out);
}
assertThat(guessFileType(certChain, terminal), is(FileType.PEM_CERT_CHAIN));
final Path tmpP12 = testRoot.resolve("tmp.p12");
assertThat(guessFileType(tmpP12, terminal), is(FileType.PKCS12));
final Path tmpJks = testRoot.resolve("tmp.jks");
assertThat(guessFileType(tmpJks, terminal), is(FileType.JKS));
final Path tmpKeystore = testRoot.resolve("tmp.keystore");
writeDummyKeystore(tmpKeystore, "PKCS12");
assertThat(guessFileType(tmpKeystore, terminal), is(FileType.PKCS12));
writeDummyKeystore(tmpKeystore, "jks");
assertThat(guessFileType(tmpKeystore, terminal), is(FileType.JKS));
}
public void testTextFileSubstitutions() throws Exception {
CheckedBiFunction<String, Map<String, String>, String, Exception> copy = (source, subs) -> {
try (InputStream in = new ByteArrayInputStream(source.getBytes(StandardCharsets.UTF_8));
StringWriter out = new StringWriter();
PrintWriter writer = new PrintWriter(out)) {
HttpCertificateCommand.copyWithSubstitutions(in, writer, subs);
return out.toString();
}
};
assertThat(copy.apply("abc\n", Collections.emptyMap()), is("abc\n"));
assertThat(copy.apply("${not_a_var}\n", Collections.emptyMap()), is("${not_a_var}\n"));
assertThat(copy.apply("${var}\n", singletonMap("var", "xyz")), is("xyz\n"));
assertThat(copy.apply("#if not\nbody\n#endif\n", Collections.emptyMap()), is(""));
assertThat(copy.apply("#if blank\nbody\n#endif\n", singletonMap("blank", "")), is(""));
assertThat(copy.apply("#if yes\nbody\n#endif\n", singletonMap("yes", "true")), is("body\n"));
assertThat(copy.apply("#if yes\ntrue\n#else\nfalse\n#endif\n", singletonMap("yes", "*")), is("true\n"));
assertThat(copy.apply("#if blank\ntrue\n#else\nfalse\n#endif\n", singletonMap("blank", "")), is("false\n"));
assertThat(copy.apply("#if var\n--> ${var} <--\n#else\n(${var})\n#endif\n", singletonMap("var", "foo")), is("--> foo <--\n"));
}
private Path getZipRoot(Path outFile) throws IOException, URISyntaxException {
assertThat(outFile, isRegularFile());
FileSystem fileSystem = FileSystems.newFileSystem(new URI("jar:" + outFile.toUri()), Collections.emptyMap());
return fileSystem.getPath("/");
}
private List<String> randomIpAddresses() throws UnknownHostException {
final int ipCount = randomIntBetween(0, 3);
final List<String> ipAddresses = new ArrayList<>(ipCount);
for (int i = 0; i < ipCount; i++) {
String ip = randomIpAddress();
ipAddresses.add(ip);
}
return ipAddresses;
}
private String randomIpAddress() throws UnknownHostException {
return formatIpAddress(randomByteArrayOfLength(4));
}
private String formatIpAddress(byte[] addr) throws UnknownHostException {
return NetworkAddress.format(InetAddress.getByAddress(addr));
}
private List<String> randomHostNames() {
final int hostCount = randomIntBetween(1, 5);
final List<String> hostNames = new ArrayList<>(hostCount);
for (int i = 0; i < hostCount; i++) {
String host = String.join(".", randomArray(1, 4, String[]::new, () -> randomAlphaOfLengthBetween(3, 8)));
if (i > 0 && randomBoolean()) {
host = "*." + host;
}
hostNames.add(host);
}
return hostNames;
}
private String randomPassword() {
// We want to assert that this password doesn't end up in any output files, so we need to make sure we
// don't randomly generate a real word.
return randomAlphaOfLength(4) + randomFrom('~', '*', '%', '$', '|') + randomAlphaOfLength(4);
}
private void verifyCertificationRequest(PKCS10CertificationRequest csr, String certificateName, List<String> hostNames,
List<String> ipAddresses) throws IOException {
// We rebuild the DN from the encoding because BC uses openSSL style toString, but we use LDAP style.
assertThat(new X500Principal(csr.getSubject().getEncoded()).toString(), is("CN=" + certificateName.replaceAll("\\.", ", DC=")));
final Attribute[] extensionAttributes = csr.getAttributes(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest);
assertThat(extensionAttributes, arrayWithSize(1));
assertThat(extensionAttributes[0].getAttributeValues(), arrayWithSize(1));
assertThat(extensionAttributes[0].getAttributeValues()[0], instanceOf(DERSequence.class));
// We register 1 extension - the subject alternative names
final Extensions extensions = Extensions.getInstance(extensionAttributes[0].getAttributeValues()[0]);
assertThat(extensions, notNullValue());
final GeneralNames names = GeneralNames.fromExtensions(extensions, Extension.subjectAlternativeName);
assertThat(names.getNames(), arrayWithSize(hostNames.size() + ipAddresses.size()));
for (GeneralName name : names.getNames()) {
assertThat(name.getTagNo(), oneOf(GeneralName.dNSName, GeneralName.iPAddress));
if (name.getTagNo() == GeneralName.dNSName) {
final String dns = DERIA5String.getInstance(name.getName()).getString();
assertThat(dns, in(hostNames));
} else if (name.getTagNo() == GeneralName.iPAddress) {
final String ip = formatIpAddress(DEROctetString.getInstance(name.getName()).getOctets());
assertThat(ip, in(ipAddresses));
}
}
}
private void verifyCertificate(X509Certificate cert, String certificateName, int years,
List<String> hostNames, List<String> ipAddresses) throws CertificateParsingException {
assertThat(cert.getSubjectX500Principal().toString(), is("CN=" + certificateName.replaceAll("\\.", ", DC=")));
final Collection<List<?>> san = cert.getSubjectAlternativeNames();
final int expectedSanEntries = hostNames.size() + ipAddresses.size();
if (expectedSanEntries > 0) {
assertThat(san, hasSize(expectedSanEntries));
for (List<?> name : san) {
assertThat(name, hasSize(2));
assertThat(name.get(0), Matchers.instanceOf(Integer.class));
assertThat(name.get(1), Matchers.instanceOf(String.class));
final Integer tag = (Integer) name.get(0);
final String value = (String) name.get(1);
assertThat(tag, oneOf(GeneralName.dNSName, GeneralName.iPAddress));
if (tag.intValue() == GeneralName.dNSName) {
assertThat(value, in(hostNames));
} else if (tag.intValue() == GeneralName.iPAddress) {
assertThat(value, in(ipAddresses));
}
}
} else if (san != null) {
assertThat(san, hasSize(0));
}
// We don't know exactly when the certificate was generated, but it should have been in the last 10 minutes
long now = System.currentTimeMillis();
long nowMinus10Minutes = now - TimeUnit.MINUTES.toMillis(10);
assertThat(cert.getNotBefore().getTime(), Matchers.lessThanOrEqualTo(now));
assertThat(cert.getNotBefore().getTime(), Matchers.greaterThanOrEqualTo(nowMinus10Minutes));
final ZonedDateTime expiry = Instant.ofEpochMilli(cert.getNotBefore().getTime()).atZone(ZoneOffset.UTC).plusYears(years);
assertThat(cert.getNotAfter().getTime(), is(expiry.toInstant().toEpochMilli()));
}
private void verifyChain(X509Certificate... chain) throws GeneralSecurityException {
for (int i = 1; i < chain.length; i++) {
assertThat(chain[i - 1].getIssuerX500Principal(), is(chain[i].getSubjectX500Principal()));
chain[i - 1].verify(chain[i].getPublicKey());
}
final X509Certificate root = chain[chain.length - 1];
assertThat(root.getIssuerX500Principal(), is(root.getSubjectX500Principal()));
}
/**
* Checks that a public + private key are a matching pair.
*/
private void assertMatchingPair(PublicKey publicKey, PrivateKey privateKey) throws GeneralSecurityException {
final byte[] bytes = randomByteArrayOfLength(128);
final Signature rsa = Signature.getInstance("SHA512withRSA");
rsa.initSign(privateKey);
rsa.update(bytes);
final byte[] signature = rsa.sign();
rsa.initVerify(publicKey);
rsa.update(bytes);
assertTrue("PublicKey and PrivateKey are not a matching pair", rsa.verify(signature));
}
private void verifyKibanaDirectory(Path zipRoot, boolean expectCAFile, Iterable<String> readmeShouldContain,
Iterable<String> shouldNotContain) throws IOException {
assertThat(zipRoot.resolve("kibana"), isDirectory());
if (expectCAFile) {
assertThat(zipRoot.resolve("kibana/elasticsearch-ca.pem"), isRegularFile());
} else {
assertThat(zipRoot.resolve("kibana/elasticsearch-ca.pem"), not(pathExists()));
}
final Path kibanaReadmePath = zipRoot.resolve("kibana/README.txt");
assertThat(kibanaReadmePath, isRegularFile());
final String kibanaReadme = new String(Files.readAllBytes(kibanaReadmePath), StandardCharsets.UTF_8);
final Path kibanaYmlPath = zipRoot.resolve("kibana/sample-kibana.yml");
assertThat(kibanaYmlPath, isRegularFile());
final String kibanaYml = new String(Files.readAllBytes(kibanaYmlPath), StandardCharsets.UTF_8);
assertThat(kibanaReadme, containsString(kibanaYmlPath.getFileName().toString()));
assertThat(kibanaReadme, containsString("elasticsearch.hosts"));
assertThat(kibanaReadme, containsString("https://"));
assertThat(kibanaReadme, containsString("elasticsearch-ca.pem"));
readmeShouldContain.forEach(s -> assertThat(kibanaReadme, containsString(s)));
shouldNotContain.forEach(s -> assertThat(kibanaReadme, not(containsString(s))));
assertThat(kibanaYml, containsString("elasticsearch.ssl.certificateAuthorities: [ \"config/elasticsearch-ca.pem\" ]"));
assertThat(kibanaYml, containsString("https://"));
shouldNotContain.forEach(s -> assertThat(kibanaYml, not(containsString(s))));
}
private PublicKey getPublicKey(PKCS10CertificationRequest pkcs) throws GeneralSecurityException {
return new JcaPKCS10CertificationRequest(pkcs).getPublicKey();
}
private int getRSAKeySize(Key key) {
assertThat(key, instanceOf(RSAKey.class));
final RSAKey rsa = (RSAKey) key;
return rsa.getModulus().bitLength();
}
private Tuple<X509Certificate, PrivateKey> readCertificateAndKey(Path pkcs12,
char[] password) throws IOException, GeneralSecurityException {
final Map<Certificate, Key> entries = CertParsingUtils.readPkcs12KeyPairs(pkcs12, password, alias -> password);
assertThat(entries.entrySet(), Matchers.hasSize(1));
Certificate cert = entries.keySet().iterator().next();
Key key = entries.get(cert);
assertThat(cert, instanceOf(X509Certificate.class));
assertThat(key, instanceOf(PrivateKey.class));
assertMatchingPair(cert.getPublicKey(), (PrivateKey) key);
return new Tuple<>((X509Certificate) cert, (PrivateKey) key);
}
private X509Certificate readPemCertificate(Path caCertPath) throws CertificateException, IOException {
final Certificate[] certificates = CertParsingUtils.readCertificates(Collections.singletonList(caCertPath));
assertThat(certificates, arrayWithSize(1));
final Certificate cert = certificates[0];
assertThat(cert, instanceOf(X509Certificate.class));
return (X509Certificate) cert;
}
private <T> T readPemObject(Path path, String expectedType,
CheckedFunction<? super byte[], T, IOException> factory) throws IOException {
assertThat(path, isRegularFile());
final PemReader csrReader = new PemReader(Files.newBufferedReader(path));
final PemObject csrPem = csrReader.readPemObject();
assertThat(csrPem.getType(), is(expectedType));
return factory.apply(csrPem.getContent());
}
private void writeDummyKeystore(Path path, String type) throws GeneralSecurityException, IOException {
Files.deleteIfExists(path);
KeyStore ks = KeyStore.getInstance(type);
ks.load(null);
if (randomBoolean()) {
final X509Certificate cert = readPemCertificate(getDataPath("ca.crt"));
ks.setCertificateEntry(randomAlphaOfLength(4), cert);
}
try (OutputStream out = Files.newOutputStream(path)) {
ks.store(out, randomAlphaOfLength(8).toCharArray());
}
}
private Environment newEnvironment() {
return TestEnvironment.newEnvironment(super.buildEnvSettings(Settings.EMPTY));
}
/**
* A special version of {@link HttpCertificateCommand} that can resolve input strings back to JIMFS paths
*/
private class PathAwareHttpCertificateCommand extends HttpCertificateCommand {
final Map<String, Path> paths;
PathAwareHttpCertificateCommand(Path... configuredPaths) {
paths = Stream.of(configuredPaths).collect(Collectors.toMap(Path::toString, Function.identity()));
}
@Override
protected Path resolvePath(String name) {
return Optional.ofNullable(this.paths.get(name)).orElseGet(() -> super.resolvePath(name));
}
}
}

View File

@ -0,0 +1,52 @@
/*
* 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.security.cli;
import org.elasticsearch.cli.SuppressForbidden;
import org.elasticsearch.xpack.core.ssl.CertParsingUtils;
import org.elasticsearch.xpack.core.ssl.PemUtils;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.util.Collections;
@SuppressForbidden(reason = "CLI utility for testing only")
public class PemToKeystore {
public static void main(String[] args) throws IOException, GeneralSecurityException {
if (args.length != 5) {
System.out.println("Usage: <java> " + PemToKeystore.class.getName() + " <keystore> <PKCS12|jks> <cert> <key> <password>");
return;
}
Path keystorePath = Paths.get(args[0]).toAbsolutePath();
String keystoreType = args[1];
Path certPath = Paths.get(args[2]).toAbsolutePath();
Path keyPath = Paths.get(args[3]).toAbsolutePath();
char[] password = args[4].toCharArray();
final Certificate[] certificates = CertParsingUtils.readCertificates(Collections.singletonList(certPath));
if (certificates.length == 0) {
throw new IllegalArgumentException("No certificates found in " + certPath);
}
final PrivateKey key = PemUtils.readPrivateKey(keyPath, () -> password);
KeyStore keyStore = KeyStore.getInstance(keystoreType);
keyStore.load(null);
keyStore.setKeyEntry("key", key, password, certificates);
try (OutputStream out = Files.newOutputStream(keystorePath)) {
keyStore.store(out, password);
}
}
}

View File

@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDSTCCAjGgAwIBAgIUe3y1qDBsjh2w16BBfPQjg5bAgjYwDQYJKoZIhvcNAQEL
BQAwNDEyMDAGA1UEAxMpRWxhc3RpYyBDZXJ0aWZpY2F0ZSBUb29sIEF1dG9nZW5l
cmF0ZWQgQ0EwHhcNMTkxMjAxMTEzNTUwWhcNMzMwODA5MTEzNTUwWjA0MTIwMAYD
VQQDEylFbGFzdGljIENlcnRpZmljYXRlIFRvb2wgQXV0b2dlbmVyYXRlZCBDQTCC
ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJdB/0UGumX8QsWAnhnadnza
HsE0WMB50j6uHgqNh/QieIw7iQGmhbwG2V+O7263j74+YOUcrjvEuR3el1+cjJIU
SP0Zl9wV2cWdltW3N/GhvU4QVnJS13w146yB3JEQROsD/hdtGP6vBGjzpjIcmKPa
pSOqJEzG113CYX260FQK86o/9kAk07kce4sx8RW+Xda/e2eLF5siIH7/7eju9OiF
RvQC1bABj0UpccuWwJWjIr93v5egTmQFHuX/Tlq44hhCKFa+0xh+LxdiAlbaeUGG
e3sd1I20veMJAOTftGCOx6Psatcw0P2+FGsliQh8MIMwkcBwkxauuUNvWZpAd+UC
AwEAAaNTMFEwHQYDVR0OBBYEFB1nLSbpN2TgSed4DBuhpwvC1CNqMB8GA1UdIwQY
MBaAFB1nLSbpN2TgSed4DBuhpwvC1CNqMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI
hvcNAQELBQADggEBABPMW7mpIi8VyA2QwwBnUo8a3OdHdospwoHm5GIbxpOHILq/
LWosNiJL/gFDVldt6nubIz/rkVqHjUf5H9DeMtlKdPCYJVZ1Cu9peEG3qPVhnTf6
G6qwt7a6/a1/AwWIc+2EaC9t6VgqVN+Dbn4dH6uI0m+ZwfsTAVCCn4tQYf9/QcRw
YHyl/C8nlWP0YwH0NDYug+J+MRewTpU+BYZPswH99HG954ZVylK00ZlQbeD6hj2w
T/P8sHl9U2vkRiGeLDhP2ygI4glXFNU5VJQGqv2GWxo9XTHCkAjGovzU8D1wYdfX
dWXUwN+qtcVdX3Ur/MowjzRumc6uWZjqEm12Vu4=
-----END CERTIFICATE-----

View File

@ -0,0 +1,30 @@
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: DES-EDE3-CBC,A4FFEDABB4598123
Dx7CtxcKyx2clKjaj/c66anbFznGXnueMkWk4FLh9nYiSQmiTqzaE/ajSzH1Fdxm
1/xBvbfFsUT6bdiGkxPEdGpgBCLnsebgjbQWv3lB0wtquhQkTfd8HoyShInnsPAb
Hu5DSHkjvjIABl1MUyHiaZVskBEj/vYKsMil2GinrpWUxwOqg0HKyrKlzb/I7Gph
Hc1NuzbRobtmQgi/JLVnOeVEIJFt3ekVQnEYQxM5+ZUsP85M6WoPOa7q8soGGLnO
OFZ20kihb20/5xaA9SUadpYWFLSwZQYN2471MXj1uWz7mEJjei81mIjdTOem486P
uIqNY1BBHzljjTq+r2mO/RKer5PRR+pbI+cNkRyESQZqitOHWWWPwXSo3K3RhhDK
gaSOSMBLv2qoYjoswafIISIMvSbcnYzNa+p1T/U62Q95STMV1ch2Ulv+20xo4nVG
3Mkr6oESB7MOcRm9XwPYZAb60MbaaFRUOagoId0AM7efLYTIpT6GXbnS0K6PPf2z
cP/LKDh3pOgzjRIAN18+nZBY7D3r6fejWsBonMPlzgEX2hBPjmOLIBgpgO3/Kg2q
+PuSE+F53fPu3t5mxsEdPtM9yJTxfughvrNCxvaxfmajmZfHaMpta1Q2H9iEhv99
L4nG1UtMJa9MMBPlTsJnkunvLcGQ8KfUMBHtlHwTwd5bP7vSs5aNGJKrdlOoKk3v
O5DGbpfw/UIw2t+2dnqwc1epkYvMJbFc7S9hYMYwJZ1BC3zHxRvBJTJ6LbCxulWC
SLUy/TZVsHSmRNftUJA48ioDSkA/inMziLmb/aqmWfvojiNmSJy/GkPJKyv1C9IK
zPqE+7noy32Cf9hztu933YBBNWPPz9Xh8WC4AluQY9Lg2H8NjBjFadL0Re0QzdBF
ZXEXT5otDthKqZpD5aRQGoleQcTYlIeJkODSgH+Ti7LvuiNJToG0iREyQRXpcsdj
iVBP3jYe4nurHRnozQIfIF0BzArSRi0aRi9PHnregS4gkLtbyKx27T61bB47TYXk
oIPm6qV7wWmVAklBz4+s3UXsTfyiqckdNxDDO+IyGEnEjpml/XePAy52hmGoQ9uI
BCAst7JC0VuKcnad9u/2BL3WN+tyhNQ1zA3OcuNLiMT3mgAghadQq2hBiO2y2cT7
b9OZLYwA4zLEzacIvo/0X1XtjiRANgZoUaMluyF5yVgnk9X9MmixBOT0pENV8GYx
WbN0xDZPPigynnQTapnLgzOzci/MQZzuWfh1wvnkiKL7y8TXGtl6AvMtYX85yrUE
Fakpleb8clKbSX2RQYlS/7+muO68e/m+svKaIS6ZupAlmu5rhlDsZAK6if+AEPpz
C4AGsV7R9aDn+TZ+Zt+cxd7s+L8rexoMthblCprv3PwCSZ75Q52iLZeajfMhcI9A
KWEra9QFT8kvIX2yuYFItuc9NL8s15zqNcaeUMyiw6gL28yBd7aQLbMj/zADOQGg
qsb5QypRsxV/neh63I7PIQfIsFOJhM3+h9xAFK48nQzc39S7b2SMYdKPOfmEOFLi
ln/q63+Bobl5EotOxb9gsQ0nWmKpQqFHsMzYQSwcJg+gGeBXy6RwIw==
-----END RSA PRIVATE KEY-----