Enable TLS trust restrictions by subject alternative name (elastic/x-pack-elasticsearch#1919)
Optional restrictions can be applied on top of an existing certificate trust scheme (PEM CAs, JKS TrustStore etc). The restrictions reduce the set of certificate that would be otherwise trusted. In this commit, the only supported restrictions are to filter by the certificate's SubjectAlternativeNames that are tagged as "other name" with an object-identifier of "cn" Original commit: elastic/x-pack-elasticsearch@c6105a47df
This commit is contained in:
parent
eb118b365c
commit
c753ddf7f2
|
@ -21,6 +21,7 @@ import static org.apache.lucene.util.automaton.Operations.DEFAULT_MAX_DETERMINIZ
|
|||
import static org.apache.lucene.util.automaton.Operations.concatenate;
|
||||
import static org.apache.lucene.util.automaton.Operations.minus;
|
||||
import static org.apache.lucene.util.automaton.Operations.union;
|
||||
import static org.elasticsearch.common.Strings.collectionToDelimitedString;
|
||||
|
||||
public final class Automatons {
|
||||
|
||||
|
@ -122,11 +123,25 @@ public final class Automatons {
|
|||
}
|
||||
|
||||
public static Predicate<String> predicate(Collection<String> patterns) {
|
||||
return predicate(patterns(patterns));
|
||||
return predicate(patterns(patterns), collectionToDelimitedString(patterns, "|"));
|
||||
}
|
||||
|
||||
public static Predicate<String> predicate(Automaton automaton) {
|
||||
return predicate(automaton, "Predicate for " + automaton);
|
||||
}
|
||||
|
||||
private static Predicate<String> predicate(Automaton automaton, final String toString) {
|
||||
CharacterRunAutomaton runAutomaton = new CharacterRunAutomaton(automaton, DEFAULT_MAX_DETERMINIZED_STATES);
|
||||
return runAutomaton::run;
|
||||
return new Predicate<String>() {
|
||||
@Override
|
||||
public boolean test(String s) {
|
||||
return runAutomaton.run(s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return toString;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,44 +5,6 @@
|
|||
*/
|
||||
package org.elasticsearch.xpack.ssl;
|
||||
|
||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
||||
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
|
||||
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
|
||||
import org.bouncycastle.asn1.x500.X500Name;
|
||||
import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier;
|
||||
import org.bouncycastle.asn1.x509.BasicConstraints;
|
||||
import org.bouncycastle.asn1.x509.Extension;
|
||||
import org.bouncycastle.asn1.x509.ExtensionsGenerator;
|
||||
import org.bouncycastle.asn1.x509.GeneralName;
|
||||
import org.bouncycastle.asn1.x509.GeneralNames;
|
||||
import org.bouncycastle.asn1.x509.Time;
|
||||
import org.bouncycastle.cert.CertIOException;
|
||||
import org.bouncycastle.cert.X509CertificateHolder;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.openssl.PEMEncryptedKeyPair;
|
||||
import org.bouncycastle.openssl.PEMKeyPair;
|
||||
import org.bouncycastle.openssl.PEMParser;
|
||||
import org.bouncycastle.openssl.X509TrustedCertificateBlock;
|
||||
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
|
||||
import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
|
||||
import org.bouncycastle.operator.ContentSigner;
|
||||
import org.bouncycastle.operator.OperatorCreationException;
|
||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
|
||||
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder;
|
||||
import org.elasticsearch.common.Nullable;
|
||||
import org.elasticsearch.common.Strings;
|
||||
import org.elasticsearch.common.SuppressForbidden;
|
||||
import org.elasticsearch.common.io.PathUtils;
|
||||
import org.elasticsearch.common.network.InetAddressHelper;
|
||||
import org.elasticsearch.common.network.NetworkAddress;
|
||||
import org.elasticsearch.env.Environment;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.DateTimeZone;
|
||||
|
||||
import javax.net.ssl.KeyManager;
|
||||
import javax.net.ssl.KeyManagerFactory;
|
||||
import javax.net.ssl.TrustManager;
|
||||
|
@ -79,12 +41,54 @@ import java.util.Locale;
|
|||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.bouncycastle.asn1.ASN1Encodable;
|
||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
||||
import org.bouncycastle.asn1.DERIA5String;
|
||||
import org.bouncycastle.asn1.DERSequence;
|
||||
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
|
||||
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
|
||||
import org.bouncycastle.asn1.x500.X500Name;
|
||||
import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier;
|
||||
import org.bouncycastle.asn1.x509.BasicConstraints;
|
||||
import org.bouncycastle.asn1.x509.Extension;
|
||||
import org.bouncycastle.asn1.x509.ExtensionsGenerator;
|
||||
import org.bouncycastle.asn1.x509.GeneralName;
|
||||
import org.bouncycastle.asn1.x509.GeneralNames;
|
||||
import org.bouncycastle.asn1.x509.Time;
|
||||
import org.bouncycastle.cert.CertIOException;
|
||||
import org.bouncycastle.cert.X509CertificateHolder;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.openssl.PEMEncryptedKeyPair;
|
||||
import org.bouncycastle.openssl.PEMKeyPair;
|
||||
import org.bouncycastle.openssl.PEMParser;
|
||||
import org.bouncycastle.openssl.X509TrustedCertificateBlock;
|
||||
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
|
||||
import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
|
||||
import org.bouncycastle.operator.ContentSigner;
|
||||
import org.bouncycastle.operator.OperatorCreationException;
|
||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
|
||||
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder;
|
||||
import org.elasticsearch.common.Nullable;
|
||||
import org.elasticsearch.common.SuppressForbidden;
|
||||
import org.elasticsearch.common.io.PathUtils;
|
||||
import org.elasticsearch.common.network.InetAddressHelper;
|
||||
import org.elasticsearch.common.network.NetworkAddress;
|
||||
import org.elasticsearch.env.Environment;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.DateTimeZone;
|
||||
|
||||
/**
|
||||
* Utility methods that deal with {@link Certificate}, {@link KeyStore}, {@link X509ExtendedTrustManager}, {@link X509ExtendedKeyManager}
|
||||
* and other certificate related objects.
|
||||
*/
|
||||
public class CertUtils {
|
||||
|
||||
static final String CN_OID = "2.5.4.3";
|
||||
|
||||
private static final int SERIAL_BIT_LENGTH = 20 * 8;
|
||||
static final BouncyCastleProvider BC_PROV = new BouncyCastleProvider();
|
||||
|
||||
|
@ -137,6 +141,7 @@ public class CertUtils {
|
|||
*/
|
||||
public static X509ExtendedTrustManager trustManager(Certificate[] certificates)
|
||||
throws NoSuchAlgorithmException, UnrecoverableKeyException, KeyStoreException, IOException, CertificateException {
|
||||
assert certificates != null : "Cannot create trust manager with null certificates";
|
||||
KeyStore store = KeyStore.getInstance("jks");
|
||||
store.load(null, null);
|
||||
int counter = 0;
|
||||
|
@ -416,4 +421,16 @@ public class CertUtils {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an X.509 {@link GeneralName} for use as a <em>Common Name</em> in the certificate's <em>Subject Alternative Names</em>
|
||||
* extension. A <em>common name</em> is a name with a tag of {@link GeneralName#otherName OTHER}, with an object-id that references
|
||||
* the {@link #CN_OID cn} attribute, and a DER encoded IA5 (ASCII) string for the name.
|
||||
* This usage of using the {@code cn} OID as a <em>Subject Alternative Name</em> is <strong>non-standard</strong> and will not be
|
||||
* recognised by other X.509/TLS implementations.
|
||||
*/
|
||||
static GeneralName createCommonName(String cn) {
|
||||
final ASN1Encodable[] sequence = { new ASN1ObjectIdentifier(CN_OID), new DERIA5String(cn) };
|
||||
return new GeneralName(GeneralName.otherName, new DERSequence(sequence));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,31 +5,6 @@
|
|||
*/
|
||||
package org.elasticsearch.xpack.ssl;
|
||||
|
||||
import joptsimple.OptionSet;
|
||||
import joptsimple.OptionSpec;
|
||||
import org.bouncycastle.asn1.DERIA5String;
|
||||
import org.bouncycastle.asn1.x509.GeneralName;
|
||||
import org.bouncycastle.asn1.x509.GeneralNames;
|
||||
import org.bouncycastle.openssl.PEMEncryptor;
|
||||
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
|
||||
import org.bouncycastle.openssl.jcajce.JcePEMEncryptorBuilder;
|
||||
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
|
||||
import org.elasticsearch.ExceptionsHelper;
|
||||
import org.elasticsearch.cli.EnvironmentAwareCommand;
|
||||
import org.elasticsearch.cli.Terminal;
|
||||
import org.elasticsearch.common.ParseField;
|
||||
import org.elasticsearch.common.Strings;
|
||||
import org.elasticsearch.common.SuppressForbidden;
|
||||
import org.elasticsearch.common.io.PathUtils;
|
||||
import org.elasticsearch.common.network.InetAddresses;
|
||||
import org.elasticsearch.common.util.set.Sets;
|
||||
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
|
||||
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
|
||||
import org.elasticsearch.common.xcontent.ObjectParser;
|
||||
import org.elasticsearch.common.xcontent.XContentParser;
|
||||
import org.elasticsearch.common.xcontent.XContentType;
|
||||
import org.elasticsearch.env.Environment;
|
||||
|
||||
import javax.security.auth.x500.X500Principal;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
@ -59,6 +34,31 @@ import java.util.regex.Pattern;
|
|||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import joptsimple.OptionSet;
|
||||
import joptsimple.OptionSpec;
|
||||
import org.bouncycastle.asn1.DERIA5String;
|
||||
import org.bouncycastle.asn1.x509.GeneralName;
|
||||
import org.bouncycastle.asn1.x509.GeneralNames;
|
||||
import org.bouncycastle.openssl.PEMEncryptor;
|
||||
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
|
||||
import org.bouncycastle.openssl.jcajce.JcePEMEncryptorBuilder;
|
||||
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
|
||||
import org.elasticsearch.ExceptionsHelper;
|
||||
import org.elasticsearch.cli.EnvironmentAwareCommand;
|
||||
import org.elasticsearch.cli.Terminal;
|
||||
import org.elasticsearch.common.ParseField;
|
||||
import org.elasticsearch.common.Strings;
|
||||
import org.elasticsearch.common.SuppressForbidden;
|
||||
import org.elasticsearch.common.io.PathUtils;
|
||||
import org.elasticsearch.common.network.InetAddresses;
|
||||
import org.elasticsearch.common.util.set.Sets;
|
||||
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
|
||||
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
|
||||
import org.elasticsearch.common.xcontent.ObjectParser;
|
||||
import org.elasticsearch.common.xcontent.XContentParser;
|
||||
import org.elasticsearch.common.xcontent.XContentType;
|
||||
import org.elasticsearch.env.Environment;
|
||||
|
||||
/**
|
||||
* CLI tool to make generation of certificates or certificate requests easier for users
|
||||
*/
|
||||
|
@ -89,11 +89,13 @@ public class CertificateTool extends EnvironmentAwareCommand {
|
|||
new ConstructingObjectParser<>(
|
||||
"instances",
|
||||
a -> new CertificateInformation(
|
||||
(String) a[0], (String) (a[1] == null ? a[0] : a[1]), (List<String>) a[2], (List<String>) a[3]));
|
||||
(String) a[0], (String) (a[1] == null ? a[0] : a[1]),
|
||||
(List<String>) a[2], (List<String>) a[3], (List<String>) a[4]));
|
||||
instanceParser.declareString(ConstructingObjectParser.constructorArg(), new ParseField("name"));
|
||||
instanceParser.declareString(ConstructingObjectParser.optionalConstructorArg(), new ParseField("filename"));
|
||||
instanceParser.declareStringArray(ConstructingObjectParser.optionalConstructorArg(), new ParseField("ip"));
|
||||
instanceParser.declareStringArray(ConstructingObjectParser.optionalConstructorArg(), new ParseField("dns"));
|
||||
instanceParser.declareStringArray(ConstructingObjectParser.optionalConstructorArg(), new ParseField("cn"));
|
||||
|
||||
PARSER.declareObjectArray(List::addAll, instanceParser, new ParseField("instances"));
|
||||
}
|
||||
|
@ -220,8 +222,9 @@ public class CertificateTool extends EnvironmentAwareCommand {
|
|||
String dnsNames = terminal.readText("Enter DNS names for instance (comma-separated if more than one) []: ");
|
||||
List<String> ipList = Arrays.asList(Strings.splitStringByCommaToArray(ipAddresses));
|
||||
List<String> dnsList = Arrays.asList(Strings.splitStringByCommaToArray(dnsNames));
|
||||
List<String> commonNames = null;
|
||||
|
||||
CertificateInformation information = new CertificateInformation(name, filename, ipList, dnsList);
|
||||
CertificateInformation information = new CertificateInformation(name, filename, ipList, dnsList, commonNames);
|
||||
List<String> validationErrors = information.validate();
|
||||
if (validationErrors.isEmpty()) {
|
||||
if (map.containsKey(name)) {
|
||||
|
@ -269,7 +272,8 @@ public class CertificateTool extends EnvironmentAwareCommand {
|
|||
fullyWriteFile(outputFile, (outputStream, pemWriter) -> {
|
||||
for (CertificateInformation certificateInformation : certInfo) {
|
||||
KeyPair keyPair = CertUtils.generateKeyPair(keysize);
|
||||
GeneralNames sanList = getSubjectAlternativeNamesValue(certificateInformation.ipAddresses, certificateInformation.dnsNames);
|
||||
GeneralNames sanList = getSubjectAlternativeNamesValue(certificateInformation.ipAddresses, certificateInformation.dnsNames,
|
||||
certificateInformation.commonNames);
|
||||
PKCS10CertificationRequest csr = CertUtils.generateCSR(keyPair, certificateInformation.name.x500Principal, sanList);
|
||||
|
||||
final String dirName = certificateInformation.name.filename + "/";
|
||||
|
@ -352,7 +356,8 @@ public class CertificateTool extends EnvironmentAwareCommand {
|
|||
for (CertificateInformation certificateInformation : certificateInformations) {
|
||||
KeyPair keyPair = CertUtils.generateKeyPair(keysize);
|
||||
Certificate certificate = CertUtils.generateSignedCertificate(certificateInformation.name.x500Principal,
|
||||
getSubjectAlternativeNamesValue(certificateInformation.ipAddresses, certificateInformation.dnsNames),
|
||||
getSubjectAlternativeNamesValue(certificateInformation.ipAddresses, certificateInformation.dnsNames,
|
||||
certificateInformation.commonNames),
|
||||
keyPair, caInfo.caCert, caInfo.privateKey, days);
|
||||
|
||||
final String dirName = certificateInformation.name.filename + "/";
|
||||
|
@ -531,7 +536,7 @@ public class CertificateTool extends EnvironmentAwareCommand {
|
|||
}
|
||||
}
|
||||
|
||||
private static GeneralNames getSubjectAlternativeNamesValue(List<String> ipAddresses, List<String> dnsNames) {
|
||||
private static GeneralNames getSubjectAlternativeNamesValue(List<String> ipAddresses, List<String> dnsNames, List<String> commonNames) {
|
||||
Set<GeneralName> generalNameList = new HashSet<>();
|
||||
for (String ip : ipAddresses) {
|
||||
generalNameList.add(new GeneralName(GeneralName.iPAddress, ip));
|
||||
|
@ -541,6 +546,10 @@ public class CertificateTool extends EnvironmentAwareCommand {
|
|||
generalNameList.add(new GeneralName(GeneralName.dNSName, dns));
|
||||
}
|
||||
|
||||
for (String cn : commonNames) {
|
||||
generalNameList.add(CertUtils.createCommonName(cn));
|
||||
}
|
||||
|
||||
if (generalNameList.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
@ -551,11 +560,13 @@ public class CertificateTool extends EnvironmentAwareCommand {
|
|||
final Name name;
|
||||
final List<String> ipAddresses;
|
||||
final List<String> dnsNames;
|
||||
final List<String> commonNames;
|
||||
|
||||
CertificateInformation(String name, String filename, List<String> ipAddresses, List<String> dnsNames) {
|
||||
CertificateInformation(String name, String filename, List<String> ipAddresses, List<String> dnsNames, List<String> commonNames) {
|
||||
this.name = Name.fromUserProvidedName(name, filename);
|
||||
this.ipAddresses = ipAddresses == null ? Collections.emptyList() : ipAddresses;
|
||||
this.dnsNames = dnsNames == null ? Collections.emptyList() : dnsNames;
|
||||
this.commonNames = commonNames == null ? Collections.emptyList() : commonNames;
|
||||
}
|
||||
|
||||
List<String> validate() {
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.xpack.ssl;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.elasticsearch.xpack.security.support.Automatons;
|
||||
|
||||
/**
|
||||
* Im memory representation of the trusted names for a "trust group".
|
||||
*
|
||||
* @see RestrictedTrustManager
|
||||
*/
|
||||
class CertificateTrustRestrictions {
|
||||
|
||||
private final Set<Predicate<String>> trustedNames;
|
||||
|
||||
CertificateTrustRestrictions(Collection<String> trustedNames) {
|
||||
this.trustedNames = trustedNames.stream().map(Automatons::predicate).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The names (X509 certificate subjectAlternateNames) of the nodes that are
|
||||
* allowed to connect to this cluster (for the targeted interface) .
|
||||
*/
|
||||
Set<Predicate<String>> getTrustedNames() {
|
||||
return Collections.unmodifiableSet(trustedNames);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "{trustedNames=" + trustedNames + '}';
|
||||
}
|
||||
}
|
|
@ -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.xpack.ssl;
|
||||
|
||||
import javax.net.ssl.X509ExtendedTrustManager;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.elasticsearch.ElasticsearchException;
|
||||
import org.elasticsearch.common.Nullable;
|
||||
import org.elasticsearch.common.io.PathUtils;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.env.Environment;
|
||||
|
||||
/**
|
||||
* An implementation of {@link TrustConfig} that constructs a {@link RestrictedTrustManager}.
|
||||
* This implementation always wraps another <code>TrustConfig</code> to perform the
|
||||
* underlying certificate validation.
|
||||
*/
|
||||
public final class RestrictedTrustConfig extends TrustConfig {
|
||||
|
||||
public static final String RESTRICTIONS_KEY_SUBJECT_NAME = "trust.subject_name";
|
||||
private final Settings settings;
|
||||
private final String groupConfigPath;
|
||||
private final TrustConfig delegate;
|
||||
|
||||
public RestrictedTrustConfig(Settings settings, String groupConfigPath, TrustConfig delegate) {
|
||||
this.settings = settings;
|
||||
this.groupConfigPath = Objects.requireNonNull(groupConfigPath);
|
||||
this.delegate = Objects.requireNonNull(delegate);
|
||||
}
|
||||
|
||||
@Override
|
||||
RestrictedTrustManager createTrustManager(@Nullable Environment environment) {
|
||||
try {
|
||||
final X509ExtendedTrustManager delegateTrustManager = delegate.createTrustManager(environment);
|
||||
final CertificateTrustRestrictions trustGroupConfig = readTrustGroup(resolveGroupConfigPath(environment));
|
||||
return new RestrictedTrustManager(settings, delegateTrustManager, trustGroupConfig);
|
||||
} catch (IOException e) {
|
||||
throw new ElasticsearchException("failed to initialize TrustManager for {}", e, toString());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
List<Path> filesToMonitor(@Nullable Environment environment) {
|
||||
return Collections.singletonList(resolveGroupConfigPath(environment));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "restrictedTrust=[" + groupConfigPath + ']';
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
RestrictedTrustConfig that = (RestrictedTrustConfig) o;
|
||||
return this.groupConfigPath.equals(that.groupConfigPath) && this.delegate.equals(that.delegate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = groupConfigPath.hashCode();
|
||||
result = 31 * result + delegate.hashCode();
|
||||
return result;
|
||||
}
|
||||
|
||||
private Path resolveGroupConfigPath(@Nullable Environment environment) {
|
||||
return CertUtils.resolvePath(groupConfigPath, environment);
|
||||
}
|
||||
|
||||
private CertificateTrustRestrictions readTrustGroup(Path path) throws IOException {
|
||||
try (InputStream in = Files.newInputStream(path)) {
|
||||
Settings settings = Settings.builder().loadFromStream(path.toString(), in).build();
|
||||
final String[] trustNodeNames = settings.getAsArray(RESTRICTIONS_KEY_SUBJECT_NAME);
|
||||
return new CertificateTrustRestrictions(Arrays.asList(trustNodeNames));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.xpack.ssl;
|
||||
|
||||
import javax.net.ssl.SSLEngine;
|
||||
import javax.net.ssl.X509ExtendedTrustManager;
|
||||
import java.net.Socket;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateParsingException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.logging.log4j.message.ParameterizedMessage;
|
||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
||||
import org.bouncycastle.asn1.ASN1Sequence;
|
||||
import org.bouncycastle.asn1.ASN1TaggedObject;
|
||||
import org.bouncycastle.asn1.DERTaggedObject;
|
||||
import org.elasticsearch.common.logging.Loggers;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
|
||||
/**
|
||||
* An X509 trust manager that only trusts connections from a restricted set of predefined network entities (nodes, clients, etc).
|
||||
* The trusted entities are defined as a list of predicates on {@link CertificateTrustRestrictions} that are applied to the
|
||||
* common-names of the certificate.
|
||||
* The common-names are read as subject-alternative-names with type 'Other' and a 'cn' OID.
|
||||
* The underlying certificate validation is delegated to another TrustManager.
|
||||
*/
|
||||
public final class RestrictedTrustManager extends X509ExtendedTrustManager {
|
||||
|
||||
private final Logger logger;
|
||||
private final X509ExtendedTrustManager delegate;
|
||||
private final CertificateTrustRestrictions trustRestrictions;
|
||||
private final int SAN_CODE_OTHERNAME = 0;
|
||||
|
||||
public RestrictedTrustManager(Settings settings, X509ExtendedTrustManager delegate, CertificateTrustRestrictions restrictions) {
|
||||
this.logger = Loggers.getLogger(getClass(), settings);
|
||||
this.delegate = delegate;
|
||||
this.trustRestrictions = restrictions;
|
||||
logger.debug("Configured with trust restrictions: [{}]", restrictions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException {
|
||||
delegate.checkClientTrusted(chain, authType, socket);
|
||||
verifyTrust(chain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException {
|
||||
delegate.checkServerTrusted(chain, authType, socket);
|
||||
verifyTrust(chain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException {
|
||||
delegate.checkClientTrusted(chain, authType, engine);
|
||||
verifyTrust(chain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException {
|
||||
delegate.checkServerTrusted(chain, authType, engine);
|
||||
verifyTrust(chain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
|
||||
delegate.checkClientTrusted(chain, authType);
|
||||
verifyTrust(chain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
|
||||
delegate.checkServerTrusted(chain, authType);
|
||||
verifyTrust(chain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
return delegate.getAcceptedIssuers();
|
||||
}
|
||||
|
||||
private void verifyTrust(X509Certificate[] chain) throws CertificateException {
|
||||
if (chain.length == 0) {
|
||||
throw new CertificateException("No certificate presented");
|
||||
}
|
||||
final X509Certificate certificate = chain[0];
|
||||
Set<String> names = readCommonNames(certificate);
|
||||
if (verifyCertificateNames(names)) {
|
||||
logger.debug(() -> new ParameterizedMessage("Trusting certificate [{}] [{}] with common-names [{}]",
|
||||
certificate.getSubjectDN(), certificate.getSerialNumber().toString(16), names));
|
||||
} else {
|
||||
logger.info("Rejecting certificate [{}] [{}] with common-names [{}]",
|
||||
certificate.getSubjectDN(), certificate.getSerialNumber().toString(16), names);
|
||||
throw new CertificateException("Certificate for " + certificate.getSubjectDN() +
|
||||
" with common-names " + names
|
||||
+ " does not match the trusted names " + trustRestrictions.getTrustedNames());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean verifyCertificateNames(Set<String> names) {
|
||||
for (Predicate<String> trust : trustRestrictions.getTrustedNames()) {
|
||||
final Optional<String> match = names.stream().filter(trust).findFirst();
|
||||
if (match.isPresent()) {
|
||||
logger.debug("Name [{}] matches trusted pattern [{}]", match.get(), trust);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Set<String> readCommonNames(X509Certificate certificate) throws CertificateParsingException {
|
||||
return getSubjectAlternativeNames(certificate).stream()
|
||||
.filter(pair -> ((Integer) pair.get(0)).intValue() == SAN_CODE_OTHERNAME)
|
||||
.map(pair -> pair.get(1))
|
||||
.map(value -> {
|
||||
ASN1Sequence seq = ASN1Sequence.getInstance(value);
|
||||
assert seq.size() == 2 : "Incorrect sequence length for 'other name'";
|
||||
final String id = ASN1ObjectIdentifier.getInstance(seq.getObjectAt(0)).getId();
|
||||
if (CertUtils.CN_OID.equals(id)) {
|
||||
final ASN1TaggedObject object = DERTaggedObject.getInstance(seq.getObjectAt(1));
|
||||
final String cn = object.getObject().toString();
|
||||
logger.trace("Read cn [{}] from ASN1Sequence [{}]", cn, seq);
|
||||
return cn;
|
||||
} else {
|
||||
logger.debug("Certificate [{}] has 'otherName' [{}] with unsupported object-id [{}]",
|
||||
certificate.getSubjectDN(), seq, id);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
|
||||
private Collection<List<?>> getSubjectAlternativeNames(X509Certificate certificate) throws CertificateParsingException {
|
||||
final Collection<List<?>> sans = certificate.getSubjectAlternativeNames();
|
||||
logger.trace("Certificate [{}] has subject alternative names [{}]", certificate.getSubjectDN(), sans);
|
||||
return sans == null ? Collections.emptyList() : sans;
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,9 @@
|
|||
*/
|
||||
package org.elasticsearch.xpack.ssl;
|
||||
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.elasticsearch.common.Nullable;
|
||||
import org.elasticsearch.common.logging.Loggers;
|
||||
import org.elasticsearch.common.settings.SecureString;
|
||||
import org.elasticsearch.common.settings.Setting;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
|
@ -39,6 +41,7 @@ public final class SSLConfiguration {
|
|||
/**
|
||||
* Creates a new SSLConfiguration from the given settings. There is no fallback configuration when invoking this constructor so
|
||||
* un-configured aspects will take on their default values.
|
||||
*
|
||||
* @param settings the SSL specific settings; only the settings under a *.ssl. prefix
|
||||
*/
|
||||
SSLConfiguration(Settings settings) {
|
||||
|
@ -53,7 +56,8 @@ public final class SSLConfiguration {
|
|||
/**
|
||||
* Creates a new SSLConfiguration from the given settings and global/default SSLConfiguration. If the settings do not contain a value
|
||||
* for a given aspect, the value from the global configuration will be used.
|
||||
* @param settings the SSL specific settings; only the settings under a *.ssl. prefix
|
||||
*
|
||||
* @param settings the SSL specific settings; only the settings under a *.ssl. prefix
|
||||
* @param globalSSLConfiguration the default configuration that is used as a fallback
|
||||
*/
|
||||
SSLConfiguration(Settings settings, SSLConfiguration globalSSLConfiguration) {
|
||||
|
@ -213,7 +217,15 @@ public final class SSLConfiguration {
|
|||
}
|
||||
|
||||
private static TrustConfig createTrustConfig(Settings settings, KeyConfig keyConfig, SSLConfiguration global) {
|
||||
final TrustConfig trustConfig = createCertChainTrustConfig(settings, keyConfig, global);
|
||||
return SETTINGS_PARSER.trustRestrictionsPath.get(settings)
|
||||
.map(path -> (TrustConfig) new RestrictedTrustConfig(settings, path, trustConfig))
|
||||
.orElse(trustConfig);
|
||||
}
|
||||
|
||||
private static TrustConfig createCertChainTrustConfig(Settings settings, KeyConfig keyConfig, SSLConfiguration global) {
|
||||
String trustStorePath = SETTINGS_PARSER.truststorePath.get(settings).orElse(null);
|
||||
|
||||
List<String> caPaths = getListOrNull(SETTINGS_PARSER.caPaths, settings);
|
||||
if (trustStorePath != null && caPaths != null) {
|
||||
throw new IllegalArgumentException("you cannot specify a truststore and ca files");
|
||||
|
|
|
@ -36,6 +36,7 @@ public class SSLConfigurationSettings {
|
|||
public final Setting<Optional<String>> truststorePath;
|
||||
public final Setting<SecureString> truststorePassword;
|
||||
public final Setting<String> truststoreAlgorithm;
|
||||
public final Setting<Optional<String>> trustRestrictionsPath;
|
||||
public final Setting<Optional<String>> keyPath;
|
||||
public final Setting<SecureString> keyPassword;
|
||||
public final Setting<Optional<String>> cert;
|
||||
|
@ -120,6 +121,11 @@ public class SSLConfigurationSettings {
|
|||
public static final Setting<String> TRUST_STORE_ALGORITHM_PROFILES = Setting.affixKeySetting("transport.profiles.",
|
||||
"xpack.security.ssl.truststore.algorithm", TRUST_STORE_ALGORITHM_TEMPLATE);
|
||||
|
||||
private static final Function<String, Setting<Optional<String>>> TRUST_RESTRICTIONS_TEMPLATE = key -> new Setting<>(key, s -> null,
|
||||
Optional::ofNullable, Property.NodeScope, Property.Filtered);
|
||||
public static final Setting<Optional<String>> TRUST_RESTRICTIONS_PROFILES = Setting.affixKeySetting("transport.profiles.",
|
||||
"xpack.security.ssl.trust_restrictions", TRUST_RESTRICTIONS_TEMPLATE);
|
||||
|
||||
private static final Function<String, Setting<SecureString>> LEGACY_KEY_PASSWORD_TEMPLATE = key -> new Setting<>(key, "",
|
||||
SecureString::new, Property.Deprecated, Property.Filtered, Property.NodeScope);
|
||||
public static final Setting<SecureString> LEGACY_KEY_PASSWORD_PROFILES = Setting.affixKeySetting("transport.profiles.",
|
||||
|
@ -173,6 +179,7 @@ public class SSLConfigurationSettings {
|
|||
truststorePassword = TRUSTSTORE_PASSWORD_TEMPLATE.apply(prefix + "truststore.secure_password");
|
||||
keystoreAlgorithm = KEY_STORE_ALGORITHM_TEMPLATE.apply(prefix + "keystore.algorithm");
|
||||
truststoreAlgorithm = TRUST_STORE_ALGORITHM_TEMPLATE.apply(prefix + "truststore.algorithm");
|
||||
trustRestrictionsPath = TRUST_RESTRICTIONS_TEMPLATE.apply(prefix + "trust_restrictions.path");
|
||||
keyPath = KEY_PATH_TEMPLATE.apply(prefix + "key");
|
||||
legacyKeyPassword = LEGACY_KEY_PASSWORD_TEMPLATE.apply(prefix + "key_passphrase");
|
||||
keyPassword = KEY_PASSWORD_TEMPLATE.apply(prefix + "secure_key_passphrase");
|
||||
|
@ -181,9 +188,11 @@ public class SSLConfigurationSettings {
|
|||
clientAuth = CLIENT_AUTH_SETTING_TEMPLATE.apply(prefix + "client_authentication");
|
||||
verificationMode = VERIFICATION_MODE_SETTING_TEMPLATE.apply(prefix + "verification_mode");
|
||||
|
||||
this.allSettings = Arrays.asList(ciphers, supportedProtocols, keystorePath, keystorePassword, keystoreAlgorithm,
|
||||
keystoreKeyPassword, truststorePath, truststorePassword, truststoreAlgorithm, keyPath, keyPassword, cert, caPaths,
|
||||
clientAuth, verificationMode, legacyKeystorePassword, legacyKeystoreKeyPassword, legacyKeyPassword, legacyTruststorePassword);
|
||||
this.allSettings = Arrays.asList(ciphers, supportedProtocols,
|
||||
keystorePath, keystorePassword, keystoreAlgorithm, keystoreKeyPassword,
|
||||
truststorePath, truststorePassword, truststoreAlgorithm, trustRestrictionsPath,
|
||||
keyPath, keyPassword, cert, caPaths, clientAuth, verificationMode,
|
||||
legacyKeystorePassword, legacyKeystoreKeyPassword, legacyKeyPassword, legacyTruststorePassword);
|
||||
}
|
||||
|
||||
public List<Setting<?>> getAllSettings() {
|
||||
|
@ -213,8 +222,8 @@ public class SSLConfigurationSettings {
|
|||
return Arrays.asList(CIPHERS_SETTING_PROFILES, SUPPORTED_PROTOCOLS_PROFILES, KEYSTORE_PATH_PROFILES,
|
||||
LEGACY_KEYSTORE_PASSWORD_PROFILES, KEYSTORE_PASSWORD_PROFILES, LEGACY_KEYSTORE_KEY_PASSWORD_PROFILES,
|
||||
KEYSTORE_KEY_PASSWORD_PROFILES, TRUST_STORE_PATH_PROFILES, LEGACY_TRUSTSTORE_PASSWORD_PROFILES,
|
||||
TRUSTSTORE_PASSWORD_PROFILES, KEY_STORE_ALGORITHM_PROFILES, TRUST_STORE_ALGORITHM_PROFILES,KEY_PATH_PROFILES,
|
||||
LEGACY_KEY_PASSWORD_PROFILES, KEY_PASSWORD_PROFILES,CERT_PROFILES,CAPATH_SETTING_PROFILES,
|
||||
TRUSTSTORE_PASSWORD_PROFILES, KEY_STORE_ALGORITHM_PROFILES, TRUST_STORE_ALGORITHM_PROFILES, TRUST_RESTRICTIONS_PROFILES,
|
||||
KEY_PATH_PROFILES, LEGACY_KEY_PASSWORD_PROFILES, KEY_PASSWORD_PROFILES,CERT_PROFILES,CAPATH_SETTING_PROFILES,
|
||||
CLIENT_AUTH_SETTING_PROFILES, VERIFICATION_MODE_SETTING_PROFILES);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -451,54 +451,8 @@ public class SSLService extends AbstractComponent {
|
|||
// if no key is provided for transport we can auto-generate a key with a signed certificate for development use only. There is a
|
||||
// bootstrap check that prevents this configuration from being use in production (SSLBootstrapCheck)
|
||||
if (transportSSLConfiguration.keyConfig() == KeyConfig.NONE) {
|
||||
// lazily generate key to avoid slowing down startup where we do not need it
|
||||
final GeneratedKeyConfig generatedKeyConfig = new GeneratedKeyConfig(settings);
|
||||
final TrustConfig trustConfig =
|
||||
new TrustConfig.CombiningTrustConfig(Arrays.asList(transportSSLConfiguration.trustConfig(), new TrustConfig() {
|
||||
@Override
|
||||
X509ExtendedTrustManager createTrustManager(@Nullable Environment environment) {
|
||||
return generatedKeyConfig.createTrustManager(environment);
|
||||
}
|
||||
createDevelopmentTLSConfiguration(sslConfigurations, transportSSLConfiguration, profileSettings);
|
||||
|
||||
@Override
|
||||
List<Path> filesToMonitor(@Nullable Environment environment) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Generated Trust Config. DO NOT USE IN PRODUCTION";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
return this == o;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return System.identityHashCode(this);
|
||||
}
|
||||
}));
|
||||
X509ExtendedTrustManager extendedTrustManager = trustConfig.createTrustManager(env);
|
||||
ReloadableTrustManager trustManager = new ReloadableTrustManager(extendedTrustManager, trustConfig);
|
||||
ReloadableX509KeyManager keyManager =
|
||||
new ReloadableX509KeyManager(generatedKeyConfig.createKeyManager(env), generatedKeyConfig);
|
||||
sslConfigurations.put(transportSSLConfiguration, createSslContext(keyManager, trustManager, transportSSLConfiguration));
|
||||
profileSettings.forEach((profileSetting) -> {
|
||||
SSLConfiguration configuration = new SSLConfiguration(profileSetting, transportSSLConfiguration);
|
||||
if (configuration.keyConfig() == KeyConfig.NONE) {
|
||||
sslConfigurations.compute(configuration, (conf, holder) -> {
|
||||
if (holder != null && holder.keyManager == keyManager && holder.trustManager == trustManager) {
|
||||
return holder;
|
||||
} else {
|
||||
return createSslContext(keyManager, trustManager, configuration);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
sslConfigurations.computeIfAbsent(configuration, this::createSslContext);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
sslConfigurations.computeIfAbsent(transportSSLConfiguration, this::createSslContext);
|
||||
profileSettings.forEach((profileSetting) ->
|
||||
|
@ -507,6 +461,60 @@ public class SSLService extends AbstractComponent {
|
|||
return Collections.unmodifiableMap(sslConfigurations);
|
||||
}
|
||||
|
||||
private void createDevelopmentTLSConfiguration(Map<SSLConfiguration, SSLContextHolder> sslConfigurations,
|
||||
SSLConfiguration transportSSLConfiguration, List<Settings> profileSettings)
|
||||
throws NoSuchAlgorithmException, IOException, CertificateException, OperatorCreationException, UnrecoverableKeyException,
|
||||
KeyStoreException {
|
||||
// lazily generate key to avoid slowing down startup where we do not need it
|
||||
final GeneratedKeyConfig generatedKeyConfig = new GeneratedKeyConfig(settings);
|
||||
final TrustConfig trustConfig =
|
||||
new TrustConfig.CombiningTrustConfig(Arrays.asList(transportSSLConfiguration.trustConfig(), new TrustConfig() {
|
||||
@Override
|
||||
X509ExtendedTrustManager createTrustManager(@Nullable Environment environment) {
|
||||
return generatedKeyConfig.createTrustManager(environment);
|
||||
}
|
||||
|
||||
@Override
|
||||
List<Path> filesToMonitor(@Nullable Environment environment) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Generated Trust Config. DO NOT USE IN PRODUCTION";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
return this == o;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return System.identityHashCode(this);
|
||||
}
|
||||
}));
|
||||
X509ExtendedTrustManager extendedTrustManager = trustConfig.createTrustManager(env);
|
||||
ReloadableTrustManager trustManager = new ReloadableTrustManager(extendedTrustManager, trustConfig);
|
||||
ReloadableX509KeyManager keyManager =
|
||||
new ReloadableX509KeyManager(generatedKeyConfig.createKeyManager(env), generatedKeyConfig);
|
||||
sslConfigurations.put(transportSSLConfiguration, createSslContext(keyManager, trustManager, transportSSLConfiguration));
|
||||
profileSettings.forEach((profileSetting) -> {
|
||||
SSLConfiguration configuration = new SSLConfiguration(profileSetting, transportSSLConfiguration);
|
||||
if (configuration.keyConfig() == KeyConfig.NONE) {
|
||||
sslConfigurations.compute(configuration, (conf, holder) -> {
|
||||
if (holder != null && holder.keyManager == keyManager && holder.trustManager == trustManager) {
|
||||
return holder;
|
||||
} else {
|
||||
return createSslContext(keyManager, trustManager, configuration);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
sslConfigurations.computeIfAbsent(configuration, this::createSslContext);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This socket factory wraps an existing SSLSocketFactory and sets the protocols and ciphers on each SSLSocket after it is created. This
|
||||
* is needed even though the SSLContext is configured properly as the configuration does not flow down to the sockets created by the
|
||||
|
|
|
@ -12,7 +12,9 @@ import org.elasticsearch.test.ESTestCase;
|
|||
import static org.apache.lucene.util.automaton.Operations.DEFAULT_MAX_DETERMINIZED_STATES;
|
||||
import static org.elasticsearch.xpack.security.support.Automatons.pattern;
|
||||
import static org.elasticsearch.xpack.security.support.Automatons.patterns;
|
||||
import static org.elasticsearch.xpack.security.support.Automatons.predicate;
|
||||
import static org.elasticsearch.xpack.security.support.Automatons.wildcard;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
|
||||
public class AutomatonsTests extends ESTestCase {
|
||||
public void testPatternsUnionOfMultiplePatterns() throws Exception {
|
||||
|
@ -53,6 +55,12 @@ public class AutomatonsTests extends ESTestCase {
|
|||
assertMatch(wildcard("t\\*st"), "t*st");
|
||||
}
|
||||
|
||||
public void testPredicateToString() throws Exception {
|
||||
assertThat(predicate("a.*z").toString(), equalTo("a.*z"));
|
||||
assertThat(predicate("a.*z", "A.*Z").toString(), equalTo("a.*z|A.*Z"));
|
||||
assertThat(predicate("a.*z", "A.*Z", "Α.*Ω").toString(), equalTo("a.*z|A.*Z|Α.*Ω"));
|
||||
}
|
||||
|
||||
private void assertMatch(Automaton automaton, String text) {
|
||||
CharacterRunAutomaton runAutomaton = new CharacterRunAutomaton(automaton, DEFAULT_MAX_DETERMINIZED_STATES);
|
||||
assertTrue(runAutomaton.run(text));
|
||||
|
|
|
@ -5,35 +5,6 @@
|
|||
*/
|
||||
package org.elasticsearch.xpack.ssl;
|
||||
|
||||
import com.google.common.jimfs.Configuration;
|
||||
import com.google.common.jimfs.Jimfs;
|
||||
import org.apache.lucene.util.IOUtils;
|
||||
import org.bouncycastle.asn1.ASN1String;
|
||||
import org.bouncycastle.asn1.DEROctetString;
|
||||
import org.bouncycastle.asn1.pkcs.Attribute;
|
||||
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
|
||||
import org.bouncycastle.asn1.x509.Extension;
|
||||
import org.bouncycastle.asn1.x509.Extensions;
|
||||
import org.bouncycastle.asn1.x509.GeneralName;
|
||||
import org.bouncycastle.asn1.x509.GeneralNames;
|
||||
import org.bouncycastle.cert.X509CertificateHolder;
|
||||
import org.bouncycastle.openssl.PEMEncryptedKeyPair;
|
||||
import org.bouncycastle.openssl.PEMParser;
|
||||
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
|
||||
import org.elasticsearch.cli.MockTerminal;
|
||||
import org.elasticsearch.common.Strings;
|
||||
import org.elasticsearch.common.SuppressForbidden;
|
||||
import org.elasticsearch.common.io.PathUtils;
|
||||
import org.elasticsearch.common.network.NetworkAddress;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.env.Environment;
|
||||
import org.elasticsearch.test.ESTestCase;
|
||||
import org.elasticsearch.test.SecuritySettingsSource;
|
||||
import org.elasticsearch.xpack.ssl.CertificateTool.CAInfo;
|
||||
import org.elasticsearch.xpack.ssl.CertificateTool.CertificateInformation;
|
||||
import org.elasticsearch.xpack.ssl.CertificateTool.Name;
|
||||
import org.junit.After;
|
||||
|
||||
import javax.security.auth.x500.X500Principal;
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
|
@ -64,8 +35,44 @@ import java.util.Set;
|
|||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.google.common.jimfs.Configuration;
|
||||
import com.google.common.jimfs.Jimfs;
|
||||
import org.apache.lucene.util.IOUtils;
|
||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
||||
import org.bouncycastle.asn1.ASN1Sequence;
|
||||
import org.bouncycastle.asn1.ASN1String;
|
||||
import org.bouncycastle.asn1.BERTags;
|
||||
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.cert.X509CertificateHolder;
|
||||
import org.bouncycastle.openssl.PEMEncryptedKeyPair;
|
||||
import org.bouncycastle.openssl.PEMParser;
|
||||
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
|
||||
import org.elasticsearch.cli.MockTerminal;
|
||||
import org.elasticsearch.common.Strings;
|
||||
import org.elasticsearch.common.SuppressForbidden;
|
||||
import org.elasticsearch.common.io.PathUtils;
|
||||
import org.elasticsearch.common.network.NetworkAddress;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.env.Environment;
|
||||
import org.elasticsearch.test.ESTestCase;
|
||||
import org.elasticsearch.test.SecuritySettingsSource;
|
||||
import org.elasticsearch.xpack.ssl.CertificateTool.CAInfo;
|
||||
import org.elasticsearch.xpack.ssl.CertificateTool.CertificateInformation;
|
||||
import org.elasticsearch.xpack.ssl.CertificateTool.Name;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.After;
|
||||
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.hamcrest.Matchers.startsWith;
|
||||
|
||||
/**
|
||||
* Unit tests for the tool used to simplify SSL certificate generation
|
||||
|
@ -179,21 +186,25 @@ public class CertificateToolTests extends ESTestCase {
|
|||
CertificateInformation certInfo = certInfosMap.get("node1");
|
||||
assertEquals(Collections.singletonList("127.0.0.1"), certInfo.ipAddresses);
|
||||
assertEquals(Collections.singletonList("localhost"), certInfo.dnsNames);
|
||||
assertEquals(Collections.emptyList(), certInfo.commonNames);
|
||||
assertEquals("node1", certInfo.name.filename);
|
||||
|
||||
certInfo = certInfosMap.get("node2");
|
||||
assertEquals(Collections.singletonList("::1"), certInfo.ipAddresses);
|
||||
assertEquals(Collections.emptyList(), certInfo.dnsNames);
|
||||
assertEquals(Collections.singletonList("node2.elasticsearch"), certInfo.commonNames);
|
||||
assertEquals("node2", certInfo.name.filename);
|
||||
|
||||
certInfo = certInfosMap.get("node3");
|
||||
assertEquals(Collections.emptyList(), certInfo.ipAddresses);
|
||||
assertEquals(Collections.emptyList(), certInfo.dnsNames);
|
||||
assertEquals(Collections.emptyList(), certInfo.commonNames);
|
||||
assertEquals("node3", certInfo.name.filename);
|
||||
|
||||
certInfo = certInfosMap.get("CN=different value");
|
||||
assertEquals(Collections.emptyList(), certInfo.ipAddresses);
|
||||
assertEquals(Collections.singletonList("node4.mydomain.com"), certInfo.dnsNames);
|
||||
assertEquals(Collections.emptyList(), certInfo.commonNames);
|
||||
assertEquals("different file", certInfo.name.filename);
|
||||
}
|
||||
|
||||
|
@ -307,7 +318,7 @@ public class CertificateToolTests extends ESTestCase {
|
|||
try (Reader reader = Files.newBufferedReader(cert)) {
|
||||
X509Certificate certificate = readX509Certificate(reader);
|
||||
assertEquals(certInfo.name.x500Principal.toString(), certificate.getSubjectX500Principal().getName());
|
||||
final int sanCount = certInfo.ipAddresses.size() + certInfo.dnsNames.size();
|
||||
final int sanCount = certInfo.ipAddresses.size() + certInfo.dnsNames.size() + certInfo.commonNames.size();
|
||||
if (sanCount == 0) {
|
||||
assertNull(certificate.getSubjectAlternativeNames());
|
||||
} else {
|
||||
|
@ -434,17 +445,25 @@ public class CertificateToolTests extends ESTestCase {
|
|||
}
|
||||
|
||||
private void assertSubjAltNames(GeneralNames subjAltNames, CertificateInformation certInfo) throws Exception {
|
||||
assertEquals(certInfo.ipAddresses.size() + certInfo.dnsNames.size(), subjAltNames.getNames().length);
|
||||
final int expectedCount = certInfo.ipAddresses.size() + certInfo.dnsNames.size() + certInfo.commonNames.size();
|
||||
assertEquals(expectedCount, subjAltNames.getNames().length);
|
||||
Collections.sort(certInfo.dnsNames);
|
||||
Collections.sort(certInfo.ipAddresses);
|
||||
for (GeneralName generalName : subjAltNames.getNames()) {
|
||||
if (generalName.getTagNo() == GeneralName.dNSName) {
|
||||
String dns = ((ASN1String)generalName.getName()).getString();
|
||||
String dns = ((ASN1String) generalName.getName()).getString();
|
||||
assertTrue(certInfo.dnsNames.stream().anyMatch(dns::equals));
|
||||
} else if (generalName.getTagNo() == GeneralName.iPAddress) {
|
||||
byte[] ipBytes = DEROctetString.getInstance(generalName.getName()).getOctets();
|
||||
String ip = NetworkAddress.format(InetAddress.getByAddress(ipBytes));
|
||||
assertTrue(certInfo.ipAddresses.stream().anyMatch(ip::equals));
|
||||
} else if (generalName.getTagNo() == GeneralName.otherName) {
|
||||
ASN1Sequence seq = ASN1Sequence.getInstance(generalName.getName());
|
||||
assertThat(seq.size(), equalTo(2));
|
||||
assertThat(seq.getObjectAt(0), instanceOf(ASN1ObjectIdentifier.class));
|
||||
assertThat(seq.getObjectAt(0).toString(), equalTo(CertUtils.CN_OID));
|
||||
assertThat(seq.getObjectAt(1), instanceOf(ASN1String.class));
|
||||
assertThat(seq.getObjectAt(1).toString(), Matchers.isIn(certInfo.commonNames));
|
||||
} else {
|
||||
fail("unknown general name with tag " + generalName.getTagNo());
|
||||
}
|
||||
|
@ -478,6 +497,8 @@ public class CertificateToolTests extends ESTestCase {
|
|||
" - name: \"node2\"",
|
||||
" filename: \"node2\"",
|
||||
" ip: \"::1\"",
|
||||
" cn:",
|
||||
" - \"node2.elasticsearch\"",
|
||||
" - name: \"node3\"",
|
||||
" filename: \"node3\"",
|
||||
" - name: \"CN=different value\"",
|
||||
|
|
|
@ -0,0 +1,178 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.xpack.ssl;
|
||||
|
||||
import javax.net.ssl.X509ExtendedTrustManager;
|
||||
import javax.security.auth.x500.X500Principal;
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyPair;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.bouncycastle.asn1.x509.GeneralName;
|
||||
import org.bouncycastle.asn1.x509.GeneralNames;
|
||||
import org.bouncycastle.operator.OperatorException;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.test.ESTestCase;
|
||||
import org.hamcrest.Description;
|
||||
import org.hamcrest.TypeSafeMatcher;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
|
||||
import static org.elasticsearch.xpack.ssl.CertUtils.generateSignedCertificate;
|
||||
|
||||
public class RestrictedTrustManagerTests extends ESTestCase {
|
||||
|
||||
/**
|
||||
* Use a small keysize for performance, since the keys are only used in this test, but a large enough keysize
|
||||
* to get past the SSL algorithm checker
|
||||
*/
|
||||
private static final int KEYSIZE = 1024;
|
||||
|
||||
private X509ExtendedTrustManager baseTrustManager;
|
||||
private Map<String, X509Certificate[]> certificates;
|
||||
private int numberOfClusters;
|
||||
private int numberOfNodes;
|
||||
|
||||
@Before
|
||||
public void generateCertificates() throws GeneralSecurityException, IOException, OperatorException {
|
||||
KeyPair caPair = CertUtils.generateKeyPair(KEYSIZE);
|
||||
X500Principal ca = new X500Principal("cn=CertAuth");
|
||||
X509Certificate caCert = CertUtils.generateCACertificate(ca, caPair, 30);
|
||||
baseTrustManager = CertUtils.trustManager(new Certificate[] { caCert });
|
||||
|
||||
certificates = new HashMap<>();
|
||||
numberOfClusters = scaledRandomIntBetween(2, 8);
|
||||
numberOfNodes = scaledRandomIntBetween(2, 8);
|
||||
for (int cluster = 1; cluster <= numberOfClusters; cluster++) {
|
||||
for (int node = 1; node <= numberOfNodes; node++) {
|
||||
KeyPair nodePair = CertUtils.generateKeyPair(KEYSIZE);
|
||||
final String cn = "n" + node + ".c" + cluster;
|
||||
final X500Principal principal = new X500Principal("cn=" + cn);
|
||||
final String san = "node" + node + ".cluster" + cluster + ".elasticsearch";
|
||||
final GeneralNames altNames = new GeneralNames(CertUtils.createCommonName(san));
|
||||
final X509Certificate signed = generateSignedCertificate(principal, altNames, nodePair, caCert, caPair.getPrivate(), 30);
|
||||
final X509Certificate self = generateSignedCertificate(principal, altNames, nodePair, null, null, 30);
|
||||
certificates.put(cn + "/ca", new X509Certificate[] { signed });
|
||||
certificates.put(cn + "/self", new X509Certificate[] { self });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void testTrustsExplicitCertificateName() throws Exception {
|
||||
final int trustedCluster = randomIntBetween(1, numberOfClusters);
|
||||
final List<String> trustedNames = new ArrayList<>(numberOfNodes);
|
||||
for (int node = 1; node <= numberOfNodes; node++) {
|
||||
trustedNames.add("node" + node + ".cluster" + trustedCluster + ".elasticsearch");
|
||||
}
|
||||
final CertificateTrustRestrictions restrictions = new CertificateTrustRestrictions(trustedNames);
|
||||
final RestrictedTrustManager trustManager = new RestrictedTrustManager(Settings.EMPTY, baseTrustManager, restrictions);
|
||||
assertSingleClusterIsTrusted(trustedCluster, trustManager, trustedNames);
|
||||
}
|
||||
|
||||
public void testTrustsWildcardCertificateName() throws Exception {
|
||||
final int trustedCluster = randomIntBetween(1, numberOfClusters);
|
||||
final List<String> trustedNames = Collections.singletonList("*.cluster" + trustedCluster + ".elasticsearch");
|
||||
final CertificateTrustRestrictions restrictions = new CertificateTrustRestrictions(trustedNames);
|
||||
final RestrictedTrustManager trustManager = new RestrictedTrustManager(Settings.EMPTY, baseTrustManager, restrictions);
|
||||
assertSingleClusterIsTrusted(trustedCluster, trustManager, trustedNames);
|
||||
}
|
||||
|
||||
public void testTrustWithRegexCertificateName() throws Exception {
|
||||
final int trustedNode = randomIntBetween(1, numberOfNodes);
|
||||
final List<String> trustedNames = Collections.singletonList("/node" + trustedNode + ".cluster[0-9].elasticsearch/");
|
||||
final CertificateTrustRestrictions restrictions = new CertificateTrustRestrictions(
|
||||
trustedNames
|
||||
);
|
||||
final RestrictedTrustManager trustManager = new RestrictedTrustManager(Settings.EMPTY, baseTrustManager, restrictions);
|
||||
for (int cluster = 1; cluster <= numberOfClusters; cluster++) {
|
||||
for (int node = 1; node <= numberOfNodes; node++) {
|
||||
if (node == trustedNode) {
|
||||
assertTrusted(trustManager, "n" + node + ".c1/ca");
|
||||
} else {
|
||||
assertNotTrusted(trustManager, "n" + node + ".c" + cluster + "/ca", trustedNames);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void testThatDelegateTrustManagerIsRespected() throws Exception {
|
||||
final CertificateTrustRestrictions restrictions = new CertificateTrustRestrictions(Collections.singletonList("*.elasticsearch"));
|
||||
final RestrictedTrustManager trustManager = new RestrictedTrustManager(Settings.EMPTY, baseTrustManager, restrictions);
|
||||
for (String cert : certificates.keySet()) {
|
||||
if (cert.endsWith("/ca")) {
|
||||
assertTrusted(trustManager, cert);
|
||||
} else {
|
||||
assertNotValid(trustManager, cert, "PKIX path building failed.*");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void assertSingleClusterIsTrusted(int trustedCluster, RestrictedTrustManager trustManager, List<String> trustedNames)
|
||||
throws Exception {
|
||||
for (int cluster = 1; cluster <= numberOfClusters; cluster++) {
|
||||
for (int node = 1; node <= numberOfNodes; node++) {
|
||||
final String certAlias = "n" + node + ".c" + cluster + "/ca";
|
||||
if (cluster == trustedCluster) {
|
||||
assertTrusted(trustManager, certAlias);
|
||||
} else {
|
||||
assertNotTrusted(trustManager, certAlias, trustedNames);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void assertTrusted(RestrictedTrustManager trustManager, String certAlias) throws Exception {
|
||||
final X509Certificate[] chain = Objects.requireNonNull(this.certificates.get(certAlias));
|
||||
try {
|
||||
trustManager.checkClientTrusted(chain, "ignore");
|
||||
// pass
|
||||
} catch (CertificateException e) {
|
||||
Assert.fail("Certificate " + describe(chain) + " is not trusted - " + e);
|
||||
}
|
||||
}
|
||||
|
||||
private void assertNotTrusted(RestrictedTrustManager trustManager, String certAlias, List<String> trustedNames) throws Exception {
|
||||
final String expectedError = ".* does not match the trusted names \\[.*" + Pattern.quote(trustedNames.get(0)) + ".*";
|
||||
assertNotValid(trustManager, certAlias, expectedError);
|
||||
}
|
||||
|
||||
private void assertNotValid(RestrictedTrustManager trustManager, String certAlias, String expectedError) throws Exception {
|
||||
final X509Certificate[] chain = Objects.requireNonNull(this.certificates.get(certAlias));
|
||||
try {
|
||||
trustManager.checkClientTrusted(chain, "ignore");
|
||||
Assert.fail("Certificate " + describe(chain) + " is trusted but shouldn't be");
|
||||
} catch (CertificateException e) {
|
||||
assertThat(e.getMessage(), new TypeSafeMatcher<String>() {
|
||||
@Override
|
||||
public void describeTo(Description description) {
|
||||
description.appendText("matches pattern ").appendText(expectedError);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean matchesSafely(String item) {
|
||||
return item.matches(expectedError);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private String describe(X509Certificate[] cert) {
|
||||
return Arrays.stream(cert).map(c -> c.getSubjectDN().getName()).collect(Collectors.joining(", "));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,259 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.xpack.ssl;
|
||||
|
||||
import javax.net.ssl.SSLHandshakeException;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.security.auth.x500.X500Principal;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.IOException;
|
||||
import java.net.SocketException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.KeyPair;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Collections;
|
||||
|
||||
import org.bouncycastle.asn1.x509.GeneralName;
|
||||
import org.bouncycastle.asn1.x509.GeneralNames;
|
||||
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
|
||||
import org.elasticsearch.ElasticsearchException;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.common.transport.TransportAddress;
|
||||
import org.elasticsearch.env.Environment;
|
||||
import org.elasticsearch.test.ESIntegTestCase;
|
||||
import org.elasticsearch.test.SecurityIntegTestCase;
|
||||
import org.elasticsearch.transport.Transport;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
|
||||
import static org.elasticsearch.xpack.ssl.CertUtils.generateSignedCertificate;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
|
||||
/**
|
||||
* Integration tests for SSL trust restrictions
|
||||
*
|
||||
* @see RestrictedTrustManager
|
||||
*/
|
||||
@ESIntegTestCase.ClusterScope(numDataNodes = 1, numClientNodes = 0, supportsDedicatedMasters = false)
|
||||
public class SSLTrustRestrictionsTests extends SecurityIntegTestCase {
|
||||
|
||||
/**
|
||||
* Use a small keysize for performance, since the keys are only used in this test, but a large enough keysize
|
||||
* to get past the SSL algorithm checker
|
||||
*/
|
||||
private static final int KEYSIZE = 1024;
|
||||
|
||||
private static final int RESOURCE_RELOAD_MILLIS = 3;
|
||||
private static final int WAIT_RELOAD_MILLIS = 25;
|
||||
|
||||
private static Path configPath;
|
||||
private static Settings nodeSSL;
|
||||
|
||||
private static CertificateInfo ca;
|
||||
private static CertificateInfo trustedCert;
|
||||
private static CertificateInfo untrustedCert;
|
||||
private static Path restrictionsPath;
|
||||
|
||||
@Override
|
||||
protected int maxNumberOfNodes() {
|
||||
// We are trying to test the SSL configuration for which clients/nodes may join a cluster
|
||||
// We prefer the cluster to only have 1 node, so that the SSL checking doesn't happen until the test methods run
|
||||
// (That's not _quite_ true, because the base setup code checks the cluster using transport client, but it's the best we can do)
|
||||
return 1;
|
||||
}
|
||||
|
||||
@BeforeClass
|
||||
public static void setupCertificates() throws Exception {
|
||||
configPath = createTempDir();
|
||||
|
||||
final KeyPair caPair = CertUtils.generateKeyPair(KEYSIZE);
|
||||
final X509Certificate caCert = CertUtils.generateCACertificate(new X500Principal("cn=CertAuth"), caPair, 30);
|
||||
ca = writeCertificates("ca", caPair.getPrivate(), caCert);
|
||||
|
||||
trustedCert = generateCertificate("trusted", "node.trusted");
|
||||
untrustedCert = generateCertificate("untrusted", "someone.else");
|
||||
|
||||
nodeSSL = Settings.builder()
|
||||
.put("xpack.security.transport.ssl.enabled", true)
|
||||
.put("xpack.security.transport.ssl.verification_mode", "certificate")
|
||||
.putArray("xpack.ssl.certificate_authorities", ca.getCertPath().toString())
|
||||
.put("xpack.ssl.key", trustedCert.getKeyPath())
|
||||
.put("xpack.ssl.certificate", trustedCert.getCertPath())
|
||||
.build();
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void cleanup() {
|
||||
configPath = null;
|
||||
nodeSSL = null;
|
||||
ca = null;
|
||||
trustedCert = null;
|
||||
untrustedCert = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Settings nodeSettings(int nodeOrdinal) {
|
||||
|
||||
Settings parentSettings = super.nodeSettings(nodeOrdinal);
|
||||
Settings.Builder builder = Settings.builder()
|
||||
.put(parentSettings.filter((s) -> s.startsWith("xpack.ssl.") == false))
|
||||
.put(nodeSSL);
|
||||
|
||||
restrictionsPath = configPath.resolve("trust_restrictions.yml");
|
||||
writeRestrictions("\"*.trusted\"");
|
||||
builder.put("xpack.ssl.trust_restrictions.path", restrictionsPath);
|
||||
builder.put("resource.reload.interval.high", RESOURCE_RELOAD_MILLIS + "ms");
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void resetRestrictions() {
|
||||
if (restrictionsPath != null) {
|
||||
writeRestrictions("\"*.trusted\"");
|
||||
}
|
||||
}
|
||||
|
||||
private void writeRestrictions(String trustedPattern) {
|
||||
try {
|
||||
Files.write(restrictionsPath, Collections.singleton("trust.subject_name: " + trustedPattern));
|
||||
} catch (IOException e) {
|
||||
throw new ElasticsearchException("failed to write restrictions", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Settings transportClientSettings() {
|
||||
Settings parentSettings = super.transportClientSettings();
|
||||
Settings.Builder builder = Settings.builder()
|
||||
.put(parentSettings.filter((s) -> s.startsWith("xpack.ssl.") == false))
|
||||
.put(nodeSSL);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean useGeneratedSSLConfig() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public void testCertificateWithTrustedNameIsAccepted() throws Exception {
|
||||
try {
|
||||
tryConnect(trustedCert);
|
||||
} catch (SSLHandshakeException | SocketException ex) {
|
||||
fail("handshake should have been successful, but failed with " + ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void testCertificateWithUntrustedNameFails() throws Exception {
|
||||
try {
|
||||
tryConnect(untrustedCert);
|
||||
fail("handshake should have failed, but was successful");
|
||||
} catch (SSLHandshakeException | SocketException ex) {
|
||||
// expected
|
||||
}
|
||||
}
|
||||
public void testRestrictionsAreReloaded() throws Exception {
|
||||
try {
|
||||
tryConnect(trustedCert);
|
||||
} catch (SSLHandshakeException | SocketException ex) {
|
||||
fail("handshake should have been successful, but failed with " + ex);
|
||||
}
|
||||
writeRestrictions("\"nothing\"");
|
||||
Thread.sleep(WAIT_RELOAD_MILLIS);
|
||||
try {
|
||||
tryConnect(trustedCert);
|
||||
fail("handshake should have failed, but was successful");
|
||||
} catch (SSLHandshakeException | SocketException ex) {
|
||||
// expected
|
||||
}
|
||||
writeRestrictions("\"*\"");
|
||||
Thread.sleep(WAIT_RELOAD_MILLIS);
|
||||
try {
|
||||
tryConnect(trustedCert);
|
||||
} catch (SSLHandshakeException | SocketException ex) {
|
||||
fail("handshake should have been successful, but failed with " + ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void tryConnect(CertificateInfo certificate) throws Exception {
|
||||
Settings settings = Settings.builder()
|
||||
.put("path.home", createTempDir())
|
||||
.put("xpack.ssl.key", certificate.getKeyPath())
|
||||
.put("xpack.ssl.certificate", certificate.getCertPath())
|
||||
.putArray("xpack.ssl.certificate_authorities", ca.getCertPath().toString())
|
||||
.put("xpack.ssl.verification_mode", "certificate")
|
||||
.build();
|
||||
|
||||
String node = randomFrom(internalCluster().getNodeNames());
|
||||
SSLService sslService = new SSLService(settings, new Environment(settings));
|
||||
SSLSocketFactory sslSocketFactory = sslService.sslSocketFactory(settings);
|
||||
TransportAddress address = internalCluster().getInstance(Transport.class, node).boundAddress().publishAddress();
|
||||
try (SSLSocket socket = (SSLSocket) sslSocketFactory.createSocket(address.getAddress(), address.getPort())) {
|
||||
assertThat(socket.isConnected(), is(true));
|
||||
// The test simply relies on this (synchronously) connecting (or not), so we don't need a handshake handler
|
||||
socket.startHandshake();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static CertificateInfo generateCertificate(String name, String san) throws Exception {
|
||||
final KeyPair keyPair = CertUtils.generateKeyPair(KEYSIZE);
|
||||
final X500Principal principal = new X500Principal("cn=" + name);
|
||||
final GeneralNames altNames = new GeneralNames(CertUtils.createCommonName(san));
|
||||
final X509Certificate cert = generateSignedCertificate(principal, altNames, keyPair, ca.getCertificate(), ca.getKey(), 30);
|
||||
return writeCertificates(name, keyPair.getPrivate(), cert);
|
||||
}
|
||||
|
||||
private static CertificateInfo writeCertificates(String name, PrivateKey key, X509Certificate cert) throws IOException {
|
||||
final Path keyPath = writePem(key, name + ".key");
|
||||
final Path certPath = writePem(cert, name + ".crt");
|
||||
return new CertificateInfo(key, keyPath, cert, certPath);
|
||||
}
|
||||
|
||||
private static Path writePem(Object obj, String filename) throws IOException {
|
||||
Path path = configPath.resolve(filename);
|
||||
Files.deleteIfExists(path);
|
||||
try (BufferedWriter out = Files.newBufferedWriter(path);
|
||||
JcaPEMWriter pemWriter = new JcaPEMWriter(out)) {
|
||||
pemWriter.writeObject(obj);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
private static class CertificateInfo {
|
||||
private final PrivateKey key;
|
||||
private final Path keyPath;
|
||||
private final X509Certificate certificate;
|
||||
private final Path certPath;
|
||||
|
||||
private CertificateInfo(PrivateKey key, Path keyPath, X509Certificate certificate, Path certPath) {
|
||||
this.key = key;
|
||||
this.keyPath = keyPath;
|
||||
this.certificate = certificate;
|
||||
this.certPath = certPath;
|
||||
}
|
||||
|
||||
private PrivateKey getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
private Path getKeyPath() {
|
||||
return keyPath;
|
||||
}
|
||||
|
||||
private X509Certificate getCertificate() {
|
||||
return certificate;
|
||||
}
|
||||
|
||||
private Path getCertPath() {
|
||||
return certPath;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue