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:
Tim Vernum 2017-07-13 23:45:00 +10:00 committed by GitHub
parent eb118b365c
commit c753ddf7f2
13 changed files with 978 additions and 156 deletions

View File

@ -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;
}
};
}
}

View File

@ -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));
}
}

View File

@ -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() {

View File

@ -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 + '}';
}
}

View File

@ -0,0 +1,90 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.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));
}
}
}

View File

@ -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;
}
}

View File

@ -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");

View File

@ -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);
}
}

View File

@ -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

View File

@ -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));

View File

@ -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\"",

View File

@ -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(", "));
}
}

View File

@ -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;
}
}
}