Skip to content
Closed
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/103596.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 103596
summary: Support optional parameter to return the JVM default trust info via _ssl/certificates
area: FIPS
type: enhancement
issues: []
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public final class DefaultJdkTrustConfig implements SslTrustConfig {

private final BiFunction<String, String, String> systemProperties;
private final char[] trustStorePassword;
private X509ExtendedTrustManager x509ExtendedTrustManager;

/**
* Create a trust config that uses System properties to determine the TrustStore type, and the relevant password.
Expand Down Expand Up @@ -61,8 +62,15 @@ public boolean isSystemDefault() {

@Override
public X509ExtendedTrustManager createTrustManager() {
if (x509ExtendedTrustManager != null) {
return x509ExtendedTrustManager;
}
try {
return KeyStoreUtil.createTrustManager(getSystemTrustStore(), TrustManagerFactory.getDefaultAlgorithm());
x509ExtendedTrustManager = KeyStoreUtil.createTrustManager(
getPasswordProtectedSystemTrustStore(),
TrustManagerFactory.getDefaultAlgorithm()
);
return x509ExtendedTrustManager;
} catch (GeneralSecurityException e) {
throw new SslConfigException("failed to initialize a TrustManager for the system keystore", e);
}
Expand All @@ -75,7 +83,7 @@ public X509ExtendedTrustManager createTrustManager() {
*
* @return the KeyStore used as truststore for PKCS#11 initialized with the password, null otherwise
*/
private KeyStore getSystemTrustStore() {
private KeyStore getPasswordProtectedSystemTrustStore() {
if (isPkcs11Truststore(systemProperties) && trustStorePassword != null) {
try {
KeyStore keyStore = KeyStore.getInstance("PKCS11");
Expand Down Expand Up @@ -106,6 +114,15 @@ public Collection<? extends StoredCertificate> getConfiguredCertificates() {
return List.of();
}

/**
* @return the certificates found in the JVM default trust store.
*/
public Collection<? extends StoredCertificate> getDefaultCertificates() {
return Arrays.stream(x509ExtendedTrustManager.getAcceptedIssuers())
.map(cert -> new StoredCertificate(cert, "default", cert.getType(), null, false))
.toList();
}

@Override
public String toString() {
return "JDK-trusted-certs";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,19 @@ public Collection<? extends StoredCertificate> getConfiguredCertificates() {
return certificates;
}

/**
* @return A collection of {@link StoredCertificate certificates} that are used by this SSL configuration iff this SSL configuration
* is using the default JRE certificates. The JRE will never ship with private keys and generally the default SSL trust certificates are
* only used if the configuration is not explicitly configured. If the default trust is not in use, then return an empty collection.
*/
public Collection<? extends StoredCertificate> getDefaultCertificates() {
List<StoredCertificate> certificates = new ArrayList<>();
if (trustConfig instanceof DefaultJdkTrustConfig defaultJdkTrustConfig) {
certificates.addAll(defaultJdkTrustConfig.getDefaultCertificates());
}
return certificates;
}

/**
* Dynamically create a new SSL context based on the current state of the configuration.
* Because the {@link #keyConfig() key config} and {@link #trustConfig() trust config} may change based on the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ public SslConfigurationLoader(String settingPrefix) {
if (this.settingPrefix.isEmpty() == false && this.settingPrefix.endsWith(".") == false) {
throw new IllegalArgumentException("Setting prefix [" + settingPrefix + "] must be blank or end in '.'");
}
this.defaultTrustConfig = new DefaultJdkTrustConfig();
this.defaultTrustConfig = DefaultJdkTrustConfig.DEFAULT_INSTANCE;
this.defaultKeyConfig = EmptyKeyConfig.INSTANCE;
this.defaultVerificationMode = SslVerificationMode.FULL;
this.defaultClientAuth = SslClientAuthenticationMode.OPTIONAL;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
}
]
},
"params":{}
"params":{
"include_defaults":{
"type":"boolean",
"description":"Whether to include the JRE default certificates (assuming any SSL context could use the default)"
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
Expand Down Expand Up @@ -690,7 +691,7 @@ private static void throwExceptionForMissingKeyMaterial(String prefix, SSLConfig

/**
* Returns information about each certificate that is referenced by any SSL configuration.
* This includes certificates used for identity (with a private key) and those used for trust, but excludes
* This includes certificates used for identity (with a private key) and those used for trust, but <b>excludes</b>
* certificates that are provided by the JRE.
* Due to the nature of KeyStores, this may include certificates that are available, but never used
* such as a CA certificate that is no longer in use, or a server certificate for an unrelated host.
Expand All @@ -706,6 +707,30 @@ public Collection<CertificateInfo> getLoadedCertificates() throws GeneralSecurit
.collect(Sets.toUnmodifiableSortedSet());
}

/**
* Returns information about each certificate that is referenced by any SSL configuration.
* This includes certificates used for identity (with a private key) and those used for trust, <b>includes</b>
* certificates that are provided by the JRE if any SSL configuration could use the default.
* Due to the nature of KeyStores, this may include certificates that are available, but never used
* such as a CA certificate that is no longer in use, or a server certificate for an unrelated host.
*
* @see SslTrustConfig#getConfiguredCertificates()
* @see SslConfiguration#getDefaultCertificates()
*/
public Collection<CertificateInfo> getAllCertificates() throws GeneralSecurityException, IOException {
return this.getLoadedSslConfigurations()
.stream()
.map(
sslConfiguration -> Stream.concat(
sslConfiguration.getConfiguredCertificates().stream(),
sslConfiguration.getDefaultCertificates().stream()
).collect(Collectors.toSet())
)
.flatMap(Collection::stream)
.map(cert -> new CertificateInfo(cert.path(), cert.format(), cert.alias(), cert.hasPrivateKey(), cert.certificate()))
.collect(Sets.toUnmodifiableSortedSet());
}

/**
* 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,9 @@
package org.elasticsearch.xpack.core.ssl.action;

import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionRequestBuilder;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.action.ActionResponse;
import org.elasticsearch.action.ActionType;
import org.elasticsearch.client.internal.ElasticsearchClient;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.xcontent.ToXContentObject;
Expand All @@ -37,10 +35,19 @@ private GetCertificateInfoAction() {

public static class Request extends ActionRequest {

Request() {}
private final boolean includeDefaults;

Request(StreamInput in) throws IOException {
public Request(boolean includeDefaults) {
this.includeDefaults = includeDefaults;
}

public Request(StreamInput in) throws IOException {
super(in);
this.includeDefaults = in.readBoolean();
}

public boolean includeDefaults() {
return includeDefaults;
}

@Override
Expand Down Expand Up @@ -85,11 +92,4 @@ public void writeTo(StreamOutput out) throws IOException {
}

}

public static class RequestBuilder extends ActionRequestBuilder<Request, Response> {
public RequestBuilder(ElasticsearchClient client) {
super(client, GetCertificateInfoAction.INSTANCE, new Request());
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ protected void doExecute(
ActionListener<GetCertificateInfoAction.Response> listener
) {
try {
Collection<CertificateInfo> certificates = sslService.getLoadedCertificates();
Collection<CertificateInfo> certificates = request.includeDefaults()
? sslService.getAllCertificates()
: sslService.getLoadedCertificates();
listener.onResponse(new GetCertificateInfoAction.Response(certificates));
} catch (GeneralSecurityException | IOException e) {
listener.onFailure(e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,16 @@ public String getName() {

@Override
protected final RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) {
return channel -> new GetCertificateInfoAction.RequestBuilder(client).execute(new RestBuilderListener<>(channel) {
@Override
public RestResponse buildResponse(Response response, XContentBuilder builder) throws Exception {
return new RestResponse(RestStatus.OK, response.toXContent(builder, request));
boolean includeDefaults = request.paramAsBoolean("include_defaults", false);
return channel -> client.execute(
GetCertificateInfoAction.INSTANCE,
new GetCertificateInfoAction.Request(includeDefaults),
new RestBuilderListener<>(channel) {
@Override
public RestResponse buildResponse(Response response, XContentBuilder builder) throws Exception {
return new RestResponse(RestStatus.OK, response.toXContent(builder, request));
}
}
});
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,20 @@
- match: { $body.0.format: "PEM" }
- match: { $body.0.has_private_key: true }
- match: { $body.0.issuer: "CN=Elasticsearch Test Node, OU=elasticsearch, O=org" }


- do:
ssl.certificates:
include_defaults: true
# in practice there are dozens of default but only ensure there are at least 10
- exists: $body.0
- exists: $body.1
- exists: $body.2
- exists: $body.3
- exists: $body.4
- exists: $body.5
- exists: $body.6
- exists: $body.7
- exists: $body.8
- exists: $body.9

Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@
}
]
},
"params":{}
"params":{
"include_defaults":{
"type":"boolean",
"description":"Whether to include the JRE default certificates (assuming any SSL context could use the default)"
}
}
}
}