|
@@ -7,6 +7,7 @@ package org.elasticsearch.xpack.security.cli;
|
|
|
|
|
|
import com.google.common.jimfs.Configuration;
|
|
|
import com.google.common.jimfs.Jimfs;
|
|
|
+import joptsimple.NonOptionArgumentSpec;
|
|
|
import joptsimple.OptionSet;
|
|
|
import joptsimple.OptionSpec;
|
|
|
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
|
@@ -21,8 +22,6 @@ 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.PEMDecryptorProvider;
|
|
|
-import org.bouncycastle.openssl.PEMEncryptedKeyPair;
|
|
|
import org.bouncycastle.openssl.PEMParser;
|
|
|
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
|
|
|
import org.elasticsearch.cli.MockTerminal;
|
|
@@ -46,11 +45,9 @@ import org.elasticsearch.xpack.security.cli.CertificateTool.CertificateInformati
|
|
|
import org.elasticsearch.xpack.security.cli.CertificateTool.GenerateCertificateCommand;
|
|
|
import org.elasticsearch.xpack.security.cli.CertificateTool.Name;
|
|
|
import org.elasticsearch.xpack.core.ssl.CertParsingUtils;
|
|
|
-import org.elasticsearch.xpack.core.ssl.PemUtils;
|
|
|
import org.hamcrest.Matchers;
|
|
|
import org.junit.After;
|
|
|
import org.junit.BeforeClass;
|
|
|
-import org.mockito.Mockito;
|
|
|
|
|
|
import javax.net.ssl.KeyManagerFactory;
|
|
|
import javax.net.ssl.TrustManagerFactory;
|
|
@@ -72,7 +69,6 @@ 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.X509Certificate;
|
|
|
import java.security.interfaces.RSAKey;
|
|
@@ -97,6 +93,9 @@ import static org.hamcrest.Matchers.in;
|
|
|
import static org.hamcrest.Matchers.instanceOf;
|
|
|
import static org.hamcrest.Matchers.is;
|
|
|
import static org.hamcrest.Matchers.nullValue;
|
|
|
+import static org.mockito.Matchers.any;
|
|
|
+import static org.mockito.Mockito.mock;
|
|
|
+import static org.mockito.Mockito.when;
|
|
|
|
|
|
/**
|
|
|
* Unit tests for the tool used to simplify SSL certificate generation
|
|
@@ -306,22 +305,17 @@ public class CertificateToolTests extends ESTestCase {
|
|
|
X509Certificate caCert = CertGenUtils.generateCACertificate(new X500Principal("CN=test ca"), keyPair, days);
|
|
|
|
|
|
final boolean selfSigned = randomBoolean();
|
|
|
- final boolean generatedCa = randomBoolean();
|
|
|
- final boolean keepCaKey = generatedCa && randomBoolean();
|
|
|
final String keyPassword = randomBoolean() ? SecuritySettingsSourceField.TEST_PASSWORD : null;
|
|
|
|
|
|
assertFalse(Files.exists(outputFile));
|
|
|
CAInfo caInfo = selfSigned ? null :
|
|
|
- new CAInfo(caCert, keyPair.getPrivate(), generatedCa, keyPassword == null ? null : keyPassword.toCharArray());
|
|
|
+ new CAInfo(caCert, keyPair.getPrivate(), false, 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 (selfSigned == false && keepCaKey) {
|
|
|
- args.add("-keep-ca-key");
|
|
|
- }
|
|
|
final OptionSet options = command.getParser().parse(Strings.toStringArray(args));
|
|
|
|
|
|
command.generateAndWriteSignedCertificates(outputFile, true, options, certInfos, caInfo, null);
|
|
@@ -335,49 +329,7 @@ public class CertificateToolTests extends ESTestCase {
|
|
|
FileSystem fileSystem = FileSystems.newFileSystem(new URI("jar:" + outputFile.toUri()), Collections.emptyMap());
|
|
|
Path zipRoot = fileSystem.getPath("/");
|
|
|
|
|
|
- if (selfSigned == false && generatedCa) {
|
|
|
- assertTrue(Files.exists(zipRoot.resolve("ca")));
|
|
|
- assertTrue(Files.exists(zipRoot.resolve("ca").resolve("ca.crt")));
|
|
|
- // check the CA cert
|
|
|
- try (InputStream input = Files.newInputStream(zipRoot.resolve("ca").resolve("ca.crt"))) {
|
|
|
- X509Certificate parsedCaCert = readX509Certificate(input);
|
|
|
- assertThat(parsedCaCert.getSubjectX500Principal().getName(), containsString("test ca"));
|
|
|
- assertEquals(caCert, parsedCaCert);
|
|
|
- long daysBetween = getDurationInDays(caCert);
|
|
|
- assertEquals(days, (int) daysBetween);
|
|
|
- }
|
|
|
-
|
|
|
- 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));
|
|
|
- // Verify we are using AES encryption
|
|
|
- final PEMDecryptorProvider pemDecryptorProvider = Mockito.mock(PEMDecryptorProvider.class);
|
|
|
- try {
|
|
|
- ((PEMEncryptedKeyPair) parsed).decryptKeyPair(pemDecryptorProvider);
|
|
|
- } catch (Exception e) {
|
|
|
- // Catch error thrown by the empty mock, we are only interested in the argument passed in
|
|
|
- }
|
|
|
- finally {
|
|
|
- Mockito.verify(pemDecryptorProvider).get("AES-128-CBC");
|
|
|
- }
|
|
|
- char[] zeroChars = new char[caInfo.password.length];
|
|
|
- Arrays.fill(zeroChars, (char) 0);
|
|
|
- assertArrayEquals(zeroChars, caInfo.password);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- PrivateKey privateKey = PemUtils.readPrivateKey(zipRoot.resolve("ca").resolve("ca.key"),
|
|
|
- () -> keyPassword != null ? keyPassword.toCharArray() : null);
|
|
|
- assertEquals(caInfo.certAndKey.key, privateKey);
|
|
|
- }
|
|
|
- } else {
|
|
|
- assertFalse(Files.exists(zipRoot.resolve("ca")));
|
|
|
- }
|
|
|
+ assertFalse(Files.exists(zipRoot.resolve("ca")));
|
|
|
|
|
|
for (CertificateInformation certInfo : certInfos) {
|
|
|
String filename = certInfo.name.filename;
|
|
@@ -413,6 +365,22 @@ public class CertificateToolTests extends ESTestCase {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ public void testErrorMessageOnInvalidKeepCaOption() {
|
|
|
+ final CertificateTool certificateTool = new CertificateTool();
|
|
|
+ final OptionSet optionSet = mock(OptionSet.class);
|
|
|
+ when(optionSet.valuesOf(any(OptionSpec.class))).thenAnswer(invocation -> {
|
|
|
+ if (invocation.getArguments()[0] instanceof NonOptionArgumentSpec) {
|
|
|
+ return List.of("cert", "--keep-ca-key");
|
|
|
+ } else {
|
|
|
+ return List.of();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ final UserException e = expectThrows(UserException.class,
|
|
|
+ () -> certificateTool.execute(new MockTerminal(), optionSet));
|
|
|
+ assertThat(e.getMessage(), containsString("Generating certificates without providing a CA is no longer supported"));
|
|
|
+ }
|
|
|
+
|
|
|
public void testGetCAInfo() throws Exception {
|
|
|
Environment env = TestEnvironment.newEnvironment(Settings.builder().put("path.home", createTempDir()).build());
|
|
|
Path testNodeCertPath = getDataPath("/org/elasticsearch/xpack/security/cli/testnode.crt");
|
|
@@ -470,17 +438,9 @@ public class CertificateToolTests extends ESTestCase {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- options = command.getParser().parse(Strings.toStringArray(args));
|
|
|
- caInfo = command.getCAInfo(terminal, options, env);
|
|
|
- caCK = caInfo.certAndKey;
|
|
|
- assertThat(terminal.getOutput(),
|
|
|
- containsString("Generating certificates without providing a CA certificate is deprecated."));
|
|
|
- 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, getKeySize(caCK.key));
|
|
|
- assertEquals(days, getDurationInDays(caCK.cert));
|
|
|
+ final OptionSet options2 = command.getParser().parse(Strings.toStringArray(args));
|
|
|
+ final UserException e = expectThrows(UserException.class, () -> command.getCAInfo(terminal, options2, env));
|
|
|
+ assertThat(e.getMessage(), containsString("Must specify either --ca or --ca-cert/--ca-key or --self-signed"));
|
|
|
|
|
|
// test self-signed
|
|
|
args = CollectionUtils.arrayAsArrayList("-self-signed");
|
|
@@ -551,7 +511,7 @@ public class CertificateToolTests extends ESTestCase {
|
|
|
* 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 or a self-signed cert
|
|
|
+ * - Creates a 3rd node certificate as a self-signed cert
|
|
|
* - 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
|
|
@@ -567,14 +527,12 @@ public class CertificateToolTests extends ESTestCase {
|
|
|
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 = randomFrom("", randomAlphaOfLengthBetween(4, 16));
|
|
|
final String node1Password = randomFrom("", randomAlphaOfLengthBetween(4, 16));
|
|
|
final String node2Password = randomFrom("", randomAlphaOfLengthBetween(4, 16));
|
|
|
final String node3Password = randomFrom("", randomAlphaOfLengthBetween(4, 16));
|
|
@@ -583,23 +541,7 @@ public class CertificateToolTests extends ESTestCase {
|
|
|
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());
|
|
|
+ final String caPassword = generateCA(caFile, terminal, env);
|
|
|
|
|
|
final GenerateCertificateCommand gen1Command = new PathAwareGenerateCertificateCommand(caFile, node1File);
|
|
|
final OptionSet gen1Options = gen1Command.getParser().parse(
|
|
@@ -640,13 +582,7 @@ public class CertificateToolTests extends ESTestCase {
|
|
|
"-dns", "node03.cluster2.es.internal.corp.net",
|
|
|
"-ip", node3Ip
|
|
|
);
|
|
|
- final boolean selfSigned = randomBoolean();
|
|
|
- if (selfSigned) {
|
|
|
- gen3Args.add("-self-signed");
|
|
|
- } else {
|
|
|
- gen3Args.add("-ca-dn");
|
|
|
- gen3Args.add("CN=My ElasticSearch Cluster 2");
|
|
|
- }
|
|
|
+ gen3Args.add("-self-signed");
|
|
|
final GenerateCertificateCommand gen3Command = new PathAwareGenerateCertificateCommand(null, node3File);
|
|
|
final OptionSet gen3Options = gen3Command.getParser().parse(
|
|
|
Strings.toStringArray(gen3Args));
|
|
@@ -688,14 +624,8 @@ public class CertificateToolTests extends ESTestCase {
|
|
|
assertThat(getKeySize(node3Key), equalTo(node3KeySize));
|
|
|
final Certificate[] certificateChain = node3KeyStore.getCertificateChain(CertificateTool.DEFAULT_CERT_NAME);
|
|
|
final X509Certificate node3x509Certificate = (X509Certificate) certificateChain[0];
|
|
|
- if (selfSigned) {
|
|
|
- assertEquals(1, certificateChain.length);
|
|
|
- assertEquals(node3x509Certificate.getSubjectX500Principal(), node3x509Certificate.getIssuerX500Principal());
|
|
|
- } else {
|
|
|
- assertEquals(2, certificateChain.length);
|
|
|
- assertEquals(node3x509Certificate.getIssuerX500Principal(),
|
|
|
- ((X509Certificate) certificateChain[1]).getSubjectX500Principal());
|
|
|
- }
|
|
|
+ assertEquals(1, certificateChain.length);
|
|
|
+ assertEquals(node3x509Certificate.getSubjectX500Principal(), node3x509Certificate.getIssuerX500Principal());
|
|
|
}
|
|
|
|
|
|
|
|
@@ -711,18 +641,21 @@ public class CertificateToolTests extends ESTestCase {
|
|
|
final MockTerminal terminal = new MockTerminal();
|
|
|
Environment env = TestEnvironment.newEnvironment(Settings.builder().put("path.home", tempDir).build());
|
|
|
|
|
|
- final Path pkcs12Zip = tempDir.resolve("p12.zip");
|
|
|
+ final Path caFile = tempDir.resolve("ca.p12");
|
|
|
+ final String caPassword = generateCA(caFile, terminal, env);
|
|
|
+
|
|
|
+ final Path node1Pkcs12 = tempDir.resolve("node1.p12");
|
|
|
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 GenerateCertificateCommand gen1Command = new PathAwareGenerateCertificateCommand(caFile, node1Pkcs12);
|
|
|
final OptionSet gen1Options = gen1Command.getParser().parse(
|
|
|
- "-keep-ca-key",
|
|
|
+ "-ca", "<ca>",
|
|
|
+ "-ca-pass", caPassword,
|
|
|
"-out", "<zip>",
|
|
|
"-keysize", String.valueOf(keySize),
|
|
|
"-days", String.valueOf(days),
|
|
@@ -730,22 +663,12 @@ public class CertificateToolTests extends ESTestCase {
|
|
|
"-name", "node01"
|
|
|
);
|
|
|
|
|
|
- terminal.addSecretInput(caPassword);
|
|
|
terminal.addSecretInput(node1Password);
|
|
|
gen1Command.execute(terminal, gen1Options, env);
|
|
|
|
|
|
- assertThat(pkcs12Zip, pathExists());
|
|
|
+ assertThat(node1Pkcs12, pathExists());
|
|
|
|
|
|
- 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());
|
|
|
-
|
|
|
- final Path node1P12 = zip1Root.resolve("node01/node01.p12");
|
|
|
- assertThat(node1P12, pathExists());
|
|
|
-
|
|
|
- final GenerateCertificateCommand gen2Command = new PathAwareGenerateCertificateCommand(caP12, pemZip);
|
|
|
+ final GenerateCertificateCommand gen2Command = new PathAwareGenerateCertificateCommand(caFile, pemZip);
|
|
|
final OptionSet gen2Options = gen2Command.getParser().parse(
|
|
|
"-ca", "<ca>",
|
|
|
"-out", "<zip>",
|
|
@@ -772,11 +695,11 @@ public class CertificateToolTests extends ESTestCase {
|
|
|
final Path node2Key = zip2Root.resolve("node02/node02.key");
|
|
|
assertThat(node2Key, pathExists());
|
|
|
|
|
|
- final KeyStore node1KeyStore = CertParsingUtils.readKeyStore(node1P12, "PKCS12", node1Password.toCharArray());
|
|
|
+ final KeyStore node1KeyStore = CertParsingUtils.readKeyStore(node1Pkcs12, "PKCS12", node1Password.toCharArray());
|
|
|
final KeyStore node1TrustStore = node1KeyStore;
|
|
|
|
|
|
final KeyStore node2KeyStore = CertParsingUtils.getKeyStoreFromPEM(node2Cert, node2Key, new char[0]);
|
|
|
- final KeyStore node2TrustStore = CertParsingUtils.readKeyStore(caP12, "PKCS12", caPassword.toCharArray());
|
|
|
+ final KeyStore node2TrustStore = CertParsingUtils.readKeyStore(caFile, "PKCS12", caPassword.toCharArray());
|
|
|
|
|
|
checkTrust(node1KeyStore, node1Password.toCharArray(), node2TrustStore, true);
|
|
|
checkTrust(node2KeyStore, new char[0], node1TrustStore, true);
|
|
@@ -808,8 +731,9 @@ public class CertificateToolTests extends ESTestCase {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
- final String optionThatTriggersZip = randomFrom("-pem", "-keep-ca-key", "-multiple", "-in=input.yml");
|
|
|
+ final String optionThatTriggersZip = randomFrom("-pem", "-multiple", "-in=input.yml");
|
|
|
final OptionSet genOptions = genCommand.getParser().parse(
|
|
|
+ "--self-signed",
|
|
|
"-out", "<zip>",
|
|
|
optionThatTriggersZip
|
|
|
);
|
|
@@ -982,4 +906,30 @@ public class CertificateToolTests extends ESTestCase {
|
|
|
return outFile;
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ private String generateCA(Path caFile, Terminal terminal, Environment env) throws Exception {
|
|
|
+ final int caKeySize = randomIntBetween(4, 8) * 512;
|
|
|
+ final int days = randomIntBetween(7, 1500);
|
|
|
+ final String caPassword = randomFrom("", randomAlphaOfLengthBetween(4, 16));
|
|
|
+
|
|
|
+ final CertificateAuthorityCommand caCommand = new CertificateAuthorityCommand() {
|
|
|
+ @Override
|
|
|
+ Path resolveOutputPath(Terminal terminal, OptionSet options, String defaultFilename) {
|
|
|
+ // 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());
|
|
|
+
|
|
|
+ return caPassword;
|
|
|
+ }
|
|
|
}
|