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:
parent
22ba759e1f
commit
2bb7b53e41
|
@ -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()) {
|
||||
|
|
|
@ -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)");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -19,6 +19,11 @@ dependencyLicenses {
|
|||
mapping from: /bc.*/, to: 'bouncycastle'
|
||||
}
|
||||
|
||||
forbiddenPatterns {
|
||||
exclude '**/*.p12'
|
||||
exclude '**/*.jks'
|
||||
}
|
||||
|
||||
rootProject.globalInfo.ready {
|
||||
if (BuildParams.inFipsJvm) {
|
||||
test.enabled = false
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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.
|
|
@ -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}".
|
|
@ -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.
|
|
@ -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" ]
|
||||
#
|
||||
|
|
@ -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}"
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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}" ]
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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-----
|
|
@ -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-----
|
Binary file not shown.
Loading…
Reference in New Issue