Skip to content

Commit

Permalink
[8.5] Support SAN/dnsName for restricted trust (#92077)
Browse files Browse the repository at this point in the history
This commit extends the TLS restricted trust model to allow reading from
alternative fields from the X509 certificate. Prior to this commit the only 
supported (hard coded) value that could be used with restricted trust
is the SAN/otherName/CN value.  This commit introduces support to read
from other fields from the X509 certificate. This commit also introduces
support to read from SAN/dnsName if configured. Any fields read from the
certificate will be used to match against the restricted trust file and if any
of the values match to the restricted trust file, then restricted trust is allowed.
Only if none of the values match then the restricted trust denied. 

SAN/otherName/CN is the default, and SAN/dnsName can be used in addition
or in place of SAN/otherName/CN. The possible configuration values are:
`*.trust_restrictions.x509_fields: ["subjectAltName.otherName.commonName", "subjectAltName.dnsName"]`

To help support testing, all of the existing certificates have been updated
to include a SAN/dnsName that matches the SAN/otherName/CN. This
allows the tests to randomize which field(s) are used to match for restricted trust.
This also has the side effect of making this commit larger than expected in
terms of lines of change. A readme has been included with copy-able commands
to recreate the certificates as needed. 

Additionally, a CCS REST test has been introduced that uses the restricted trust. 
To support this new CCS REST test the private keys for the test certificates are also
included in this commit as well as the gradle configuration needed to share those
certificates across projects. 

Co-authored-by: Jake Landis <[email protected]>
  • Loading branch information
tvernum and jakelandis authored Dec 5, 2022
1 parent 4a9116a commit 4ed5ee9
Show file tree
Hide file tree
Showing 271 changed files with 6,181 additions and 1,761 deletions.
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: []
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,10 @@ protected Path resolvePath(String settingKey, Path basePath) {
return resolveSetting(settingKey, basePath::resolve, null);
}

protected List<String> resolveList(String settingKey, List<String> defaultList) {
return resolveListSetting(settingKey, Function.identity(), defaultList);
}

private String expandSettingKey(String key) {
return settingPrefix + key;
}
Expand Down
7 changes: 7 additions & 0 deletions x-pack/plugin/core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,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-grok')
Expand Down Expand Up @@ -59,6 +64,8 @@ dependencies {

yamlRestTestImplementation project(':x-pack:plugin:core')
javaRestTestImplementation(testArtifact(project(xpackModule('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 @@ -17,6 +17,7 @@
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Set;

import javax.net.ssl.X509ExtendedTrustManager;

Expand All @@ -28,10 +29,15 @@
public final class RestrictedTrustConfig implements SslTrustConfig {

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 = Set.of(SAN_OTHER_COMMON, SAN_DNS);
private final Path groupConfigPath;
private final SslTrustConfig delegate;
private final Set<String> configuredX509Fields;

RestrictedTrustConfig(Path groupConfigPath, SslTrustConfig delegate) {
RestrictedTrustConfig(Path groupConfigPath, Set<String> configuredX509Fields, SslTrustConfig delegate) {
this.configuredX509Fields = configuredX509Fields;
this.groupConfigPath = Objects.requireNonNull(groupConfigPath);
this.delegate = Objects.requireNonNull(delegate);
}
Expand All @@ -41,7 +47,7 @@ public RestrictedTrustManager createTrustManager() {
try {
final X509ExtendedTrustManager delegateTrustManager = delegate.createTrustManager();
final CertificateTrustRestrictions trustGroupConfig = readTrustGroup(groupConfigPath);
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 @@ -28,26 +30,39 @@
import javax.net.ssl.X509ExtendedTrustManager;

import static org.elasticsearch.core.Strings.format;
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 @@ -96,28 +111,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(
() -> format(
"Trusting certificate [%s] [%s] with common-names [%s]",
"Trusting certificate [%s] [%s] with fields [%s] with values [%s]",
certificate.getSubjectX500Principal(),
certificate.getSerialNumber().toString(16),
names
x509Fields,
values
)
);
} else {
logger.info(
"Rejecting certificate [{}] [{}] with common-names [{}]",
"Rejecting certificate [{}] [{}] for fields [{}] with values [{}]",
certificate.getSubjectX500Principal(),
certificate.getSerialNumber().toString(16),
names
x509Fields,
values
);
throw new CertificateException(
"Certificate for "
+ certificate.getSubjectX500Principal()
+ " with common-names "
+ names
+ " with fields "
+ x509Fields
+ " with values "
+ values
+ " does not match the trusted names "
+ trustRestrictions.getTrustedNames()
);
Expand All @@ -135,13 +154,28 @@ private boolean verifyCertificateNames(Set<String> names) {
return false;
}

private static 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 @@ -12,6 +12,7 @@
import org.elasticsearch.common.settings.Setting.Property;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.ssl.SslClientAuthenticationMode;
import org.elasticsearch.common.ssl.SslConfigException;
import org.elasticsearch.common.ssl.SslConfigurationKeys;
import org.elasticsearch.common.ssl.SslVerificationMode;
import org.elasticsearch.common.util.CollectionUtils;
Expand Down Expand Up @@ -43,6 +44,7 @@ public class SSLConfigurationSettings {
final Setting<String> truststoreAlgorithm;
final Setting<Optional<String>> truststoreType;
final Setting<Optional<String>> trustRestrictionsPath;
final Setting<List<String>> trustRestrictionsX509Fields;
final Setting<List<String>> caPaths;
final Setting<Optional<SslClientAuthenticationMode>> clientAuth;
final Setting<Optional<SslVerificationMode>> verificationMode;
Expand Down Expand Up @@ -143,16 +145,43 @@ public class SSLConfigurationSettings {
TRUST_STORE_TYPE_TEMPLATE
);

private static final Function<String, Setting<Optional<String>>> TRUST_RESTRICTIONS_TEMPLATE = key -> new Setting<>(
private static final Function<String, Setting<Optional<String>>> TRUST_RESTRICTIONS_PATH_TEMPLATE = key -> new Setting<>(
key,
s -> null,
Optional::ofNullable,
Property.NodeScope,
Property.Filtered
);
private static final SslSetting<Optional<String>> TRUST_RESTRICTIONS = SslSetting.setting(
private static final SslSetting<Optional<String>> TRUST_RESTRICTIONS_PATH = SslSetting.setting(
"trust_restrictions.path",
TRUST_RESTRICTIONS_TEMPLATE
TRUST_RESTRICTIONS_PATH_TEMPLATE
);

public static final Function<String, Setting<List<String>>> TRUST_RESTRICTIONS_X509_FIELDS_TEMPLATE = key -> Setting.listSetting(
key,
List.of("subjectAltName.otherName.commonName"),
s -> {
RestrictedTrustConfig.SUPPORTED_X_509_FIELDS.stream()
.filter(v -> v.equalsIgnoreCase(s))
.findAny()
.ifPresentOrElse(v -> {}, () -> {
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 SslSetting<List<String>> TRUST_RESTRICTIONS_X509_FIELDS = SslSetting.setting(
"trust_restrictions.x509_fields",
TRUST_RESTRICTIONS_X509_FIELDS_TEMPLATE
);

private static final SslSetting<SecureString> LEGACY_KEY_PASSWORD = SslSetting.setting(
Expand Down Expand Up @@ -228,7 +257,8 @@ private SSLConfigurationSettings(String prefix, boolean acceptNonSecurePasswords
truststorePassword = TRUSTSTORE_PASSWORD.withPrefix(prefix);
truststoreAlgorithm = TRUSTSTORE_ALGORITHM.withPrefix(prefix);
truststoreType = TRUSTSTORE_TYPE.withPrefix(prefix);
trustRestrictionsPath = TRUST_RESTRICTIONS.withPrefix(prefix);
trustRestrictionsPath = TRUST_RESTRICTIONS_PATH.withPrefix(prefix);
trustRestrictionsX509Fields = TRUST_RESTRICTIONS_X509_FIELDS.withPrefix(prefix);
caPaths = CERT_AUTH_PATH.withPrefix(prefix);
clientAuth = CLIENT_AUTH_SETTING.withPrefix(prefix);
verificationMode = VERIFICATION_MODE.withPrefix(prefix);
Expand All @@ -241,6 +271,7 @@ private SSLConfigurationSettings(String prefix, boolean acceptNonSecurePasswords
truststoreAlgorithm,
truststoreType,
trustRestrictionsPath,
trustRestrictionsX509Fields,
caPaths,
clientAuth,
verificationMode
Expand Down Expand Up @@ -304,7 +335,8 @@ private static Collection<SslSetting<?>> settings() {
TRUSTSTORE_ALGORITHM,
KEY_STORE_TYPE,
TRUSTSTORE_TYPE,
TRUST_RESTRICTIONS,
TRUST_RESTRICTIONS_PATH,
TRUST_RESTRICTIONS_X509_FIELDS,
KEY_PATH,
LEGACY_KEY_PASSWORD,
KEY_PASSWORD,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,13 @@
import java.security.KeyStore;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import static org.elasticsearch.xpack.core.ssl.SSLConfigurationSettings.TRUST_RESTRICTIONS_X509_FIELDS;
import static org.elasticsearch.xpack.core.ssl.SSLConfigurationSettings.TRUST_RESTRICTIONS_X509_FIELDS_TEMPLATE;

/**
* A configuration loader for SSL Settings
*/
Expand Down Expand Up @@ -117,7 +121,16 @@ protected SslTrustConfig buildTrustConfig(Path basePath, SslVerificationMode ver
if (trustRestrictions == null) {
return trustConfig;
}
return new RestrictedTrustConfig(trustRestrictions, trustConfig);
return new RestrictedTrustConfig(
trustRestrictions,
Set.copyOf(
super.resolveList(
TRUST_RESTRICTIONS_X509_FIELDS.rawSetting().getKey(),
TRUST_RESTRICTIONS_X509_FIELDS_TEMPLATE.apply("").getDefault(settings)
)
),
trustConfig
);
}

public SslConfiguration load(Environment env) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,12 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;

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 @@ -68,7 +71,7 @@ public int hashCode() {
}
};

final RestrictedTrustConfig restrictedTrustConfig = new RestrictedTrustConfig(groupConfigPath, delegate);
final RestrictedTrustConfig restrictedTrustConfig = new RestrictedTrustConfig(groupConfigPath, Set.of(SAN_OTHER_COMMON), delegate);
Collection<Path> filesToMonitor = restrictedTrustConfig.getDependentFiles();
List<Path> expectedPathList = new ArrayList<>(otherFiles);
expectedPathList.add(groupConfigPath);
Expand Down
Loading

0 comments on commit 4ed5ee9

Please sign in to comment.