Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/changelog/91946.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 91946
summary: Support SAN/dnsName for restricted trust
area: TLS
type: enhancement
issues: []
7 changes: 7 additions & 0 deletions x-pack/plugin/core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ tasks.named("dependencyLicenses").configure {
mapping from: /commons-.*/, to: 'commons' // pulled in by rest client
}

configurations {
signedCerts
rootCert
}

dependencies {
compileOnly project(":server")
api project(":libs:elasticsearch-ssl-config")
Expand Down Expand Up @@ -62,6 +67,8 @@ dependencies {

yamlRestTestImplementation project(':x-pack:plugin:core')

signedCerts fileTree("src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed")
rootCert files("src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca.crt")
}

ext.expansions = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Set;

import javax.net.ssl.X509ExtendedTrustManager;

Expand All @@ -30,10 +31,15 @@
public final class RestrictedTrustConfig extends TrustConfig {

private static final String RESTRICTIONS_KEY_SUBJECT_NAME = "trust.subject_name";
public static final String SAN_OTHER_COMMON = "subjectAltName.otherName.commonName";
public static final String SAN_DNS = "subjectAltName.dnsName";
static final Set<String> SUPPORTED_X_509_FIELDS = org.elasticsearch.core.Set.of(SAN_OTHER_COMMON, SAN_DNS);
private final String groupConfigPath;
private final TrustConfig delegate;
private final Set<String> configuredX509Fields;

RestrictedTrustConfig(String groupConfigPath, TrustConfig delegate) {
RestrictedTrustConfig(String groupConfigPath, Set<String> configuredX509Fields, TrustConfig delegate) {
this.configuredX509Fields = configuredX509Fields;
this.groupConfigPath = Objects.requireNonNull(groupConfigPath);
this.delegate = Objects.requireNonNull(delegate);
}
Expand All @@ -43,7 +49,7 @@ RestrictedTrustManager createTrustManager(@Nullable Environment environment) {
try {
final X509ExtendedTrustManager delegateTrustManager = delegate.createTrustManager(environment);
final CertificateTrustRestrictions trustGroupConfig = readTrustGroup(resolveGroupConfigPath(environment));
return new RestrictedTrustManager(delegateTrustManager, trustGroupConfig);
return new RestrictedTrustManager(delegateTrustManager, trustGroupConfig, configuredX509Fields);
} catch (IOException e) {
throw new ElasticsearchException("failed to initialize TrustManager for {}", e, toString());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
Expand All @@ -27,25 +29,39 @@
import javax.net.ssl.SSLEngine;
import javax.net.ssl.X509ExtendedTrustManager;

import static org.elasticsearch.xpack.core.ssl.RestrictedTrustConfig.SAN_DNS;
import static org.elasticsearch.xpack.core.ssl.RestrictedTrustConfig.SAN_OTHER_COMMON;

/**
* 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.
* The trusted entities are defined as a list of predicates on {@link CertificateTrustRestrictions} that built from the
* configured restricted trust file. The values in the restricted trust file are compared to value(s) read from the X509 certificate.
* If the value(s) read from the X509 certificate match values configured in restricted trust file then restricted trust is established.
* If there is no match, then restricted trust is not established and the connection should be terminated. Restricted trust should be used
* in conjunction with additional trust models and is intended to restrict, not provide trust.
* The values read from the X509 certificate are configurable and the following are supported:
* <ul>
* <li>subjectAltName.otherName.commonName</li>
* <li>subjectAltName.dnsName</li>
* </ul>
* see also: {@link RestrictedTrustConfig}
*/
public final class RestrictedTrustManager extends X509ExtendedTrustManager {
private static final Logger logger = LogManager.getLogger(RestrictedTrustManager.class);
private static final String CN_OID = "2.5.4.3";
private static final int SAN_CODE_OTHERNAME = 0;
private static final int SAN_CODE_DNS = 2;

private final X509ExtendedTrustManager delegate;
private final CertificateTrustRestrictions trustRestrictions;
private final Set<String> x509Fields;

public RestrictedTrustManager(X509ExtendedTrustManager delegate, CertificateTrustRestrictions restrictions) {
public RestrictedTrustManager(X509ExtendedTrustManager delegate, CertificateTrustRestrictions restrictions, Set<String> x509Fields) {
this.delegate = delegate;
this.trustRestrictions = restrictions;
this.x509Fields = x509Fields.stream().map(s -> s.toLowerCase(Locale.ROOT)).collect(Collectors.toSet());
logger.debug("Configured with trust restrictions: [{}]", restrictions);
logger.debug("Configured with x509 fields: [{}]", x509Fields);
}

@Override
Expand Down Expand Up @@ -94,28 +110,32 @@ private void verifyTrust(X509Certificate[] chain) throws CertificateException {
throw new CertificateException("No certificate presented");
}
final X509Certificate certificate = chain[0];
Set<String> names = readCommonNames(certificate);
if (verifyCertificateNames(names)) {
Set<String> values = readX509Certificate(certificate);
if (verifyCertificateNames(values)) {
logger.debug(
() -> new ParameterizedMessage(
"Trusting certificate [{}] [{}] with common-names [{}]",
"Trusting certificate [{}] [{}] with fields [{}] with values [{}]",
certificate.getSubjectDN(),
certificate.getSerialNumber().toString(16),
names
x509Fields,
values
)
);
} else {
logger.info(
"Rejecting certificate [{}] [{}] with common-names [{}]",
"Rejecting certificate [{}] [{}] for fields [{}] with values [{}]",
certificate.getSubjectDN(),
certificate.getSerialNumber().toString(16),
names
x509Fields,
values
);
throw new CertificateException(
"Certificate for "
+ certificate.getSubjectDN()
+ " with common-names "
+ names
+ " with fields "
+ x509Fields
+ " with values "
+ values
+ " does not match the trusted names "
+ trustRestrictions.getTrustedNames()
);
Expand All @@ -133,13 +153,28 @@ private boolean verifyCertificateNames(Set<String> names) {
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 -> decodeDerValue((byte[]) value, certificate))
.filter(Objects::nonNull)
.collect(Collectors.toSet());
private Set<String> readX509Certificate(X509Certificate certificate) throws CertificateParsingException {
Collection<List<?>> sans = getSubjectAlternativeNames(certificate);
Set<String> values = new HashSet<>();
if (x509Fields.contains(SAN_DNS.toLowerCase(Locale.ROOT))) {
Set<String> dnsNames = sans.stream()
.filter(pair -> ((Integer) pair.get(0)).intValue() == SAN_CODE_DNS)
.map(pair -> pair.get(1))
.map(Object::toString)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
values.addAll(dnsNames);
}
if (x509Fields.contains(SAN_OTHER_COMMON.toLowerCase(Locale.ROOT))) {
Set<String> otherNames = getSubjectAlternativeNames(certificate).stream()
.filter(pair -> ((Integer) pair.get(0)).intValue() == SAN_CODE_OTHERNAME)
.map(pair -> pair.get(1))
.map(value -> decodeDerValue((byte[]) value, certificate))
.filter(Objects::nonNull)
.collect(Collectors.toSet());
values.addAll(otherNames);
}
return values;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,13 @@ private static KeyConfig createKeyConfig(Settings settings) {
private static TrustConfig createTrustConfig(Settings settings, KeyConfig keyConfig) {
final TrustConfig trustConfig = createCertChainTrustConfig(settings, keyConfig);
return SETTINGS_PARSER.trustRestrictionsPath.get(settings)
.map(path -> (TrustConfig) new RestrictedTrustConfig(path, trustConfig))
.map(
path -> (TrustConfig) new RestrictedTrustConfig(
path,
org.elasticsearch.core.Set.copyOf(SETTINGS_PARSER.trustRestrictionsX509Fields.get(settings)),
trustConfig
)
)
.orElse(trustConfig);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Setting.Property;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.ssl.SslConfigException;
import org.elasticsearch.common.util.CollectionUtils;

import java.security.KeyStore;
Expand Down Expand Up @@ -41,6 +42,7 @@ public class SSLConfigurationSettings {
public final Setting<String> truststoreAlgorithm;
public final Setting<Optional<String>> truststoreType;
public final Setting<Optional<String>> trustRestrictionsPath;
public final Setting<List<String>> trustRestrictionsX509Fields;
public final Setting<List<String>> caPaths;
public final Setting<Optional<SSLClientAuth>> clientAuth;
public final Setting<Optional<VerificationMode>> verificationMode;
Expand Down Expand Up @@ -264,9 +266,30 @@ public class SSLConfigurationSettings {
"xpack.security.ssl.trust_restrictions",
TRUST_RESTRICTIONS_TEMPLATE
);

public static final Function<String, Setting.AffixSetting<Optional<String>>> TRUST_RESTRICTIONS_REALM = realmType -> Setting
.affixKeySetting("xpack.security.authc.realms." + realmType + ".", "ssl.trust_restrictions", TRUST_RESTRICTIONS_TEMPLATE);

public static final Function<String, Setting<List<String>>> TRUST_RESTRICTIONS_X509_FIELDS_TEMPLATE = key -> Setting.listSetting(
key,
org.elasticsearch.core.List.of("subjectAltName.otherName.commonName"),
s -> {
Optional<String> value = RestrictedTrustConfig.SUPPORTED_X_509_FIELDS.stream().filter(v -> v.equalsIgnoreCase(s)).findAny();
if (value.isPresent() == false) {
throw new SslConfigException(
s
+ " is not a supported x509 field for trust restrictions. "
+ "Recognised values are ["
+ String.join(",", RestrictedTrustConfig.SUPPORTED_X_509_FIELDS)
+ "]"
);
}
return s;
},
Property.NodeScope,
Property.Filtered
);

public static final Setting<SecureString> LEGACY_KEY_PASSWORD_PROFILES = Setting.affixKeySetting(
"transport.profiles.",
"xpack.security.ssl.key_passphrase",
Expand Down Expand Up @@ -371,6 +394,7 @@ private SSLConfigurationSettings(String prefix) {
truststoreAlgorithm = TRUST_STORE_ALGORITHM_TEMPLATE.apply(prefix + "truststore.algorithm");
truststoreType = TRUST_STORE_TYPE_TEMPLATE.apply(prefix + "truststore.type");
trustRestrictionsPath = TRUST_RESTRICTIONS_TEMPLATE.apply(prefix + "trust_restrictions.path");
trustRestrictionsX509Fields = TRUST_RESTRICTIONS_X509_FIELDS_TEMPLATE.apply(prefix + "trust_restrictions.x509_fields");
caPaths = CAPATH_SETTING_TEMPLATE.apply(prefix + "certificate_authorities");
clientAuth = CLIENT_AUTH_SETTING_TEMPLATE.apply(prefix + "client_authentication");
verificationMode = VERIFICATION_MODE_SETTING_TEMPLATE.apply(prefix + "verification_mode");
Expand All @@ -383,6 +407,7 @@ private SSLConfigurationSettings(String prefix) {
truststoreAlgorithm,
truststoreType,
trustRestrictionsPath,
trustRestrictionsX509Fields,
caPaths,
clientAuth,
verificationMode,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@

import javax.net.ssl.X509ExtendedTrustManager;

import static org.elasticsearch.xpack.core.ssl.RestrictedTrustConfig.SAN_OTHER_COMMON;

public class RestrictedTrustConfigTests extends ESTestCase {

public void testDelegationOfFilesToMonitor() throws Exception {
Expand Down Expand Up @@ -70,7 +72,11 @@ public int hashCode() {
}
};

final RestrictedTrustConfig restrictedTrustConfig = new RestrictedTrustConfig(groupConfigPath.toString(), delegate);
final RestrictedTrustConfig restrictedTrustConfig = new RestrictedTrustConfig(
groupConfigPath.toString(),
org.elasticsearch.core.Set.of(SAN_OTHER_COMMON),
delegate
);
List<Path> filesToMonitor = restrictedTrustConfig.filesToMonitor(environment);
List<Path> expectedPathList = new ArrayList<>(otherFiles);
expectedPathList.add(groupConfigPath);
Expand Down
Loading