diff --git a/docs/changelog/103596.yaml b/docs/changelog/103596.yaml new file mode 100644 index 0000000000000..2d106cc53e5eb --- /dev/null +++ b/docs/changelog/103596.yaml @@ -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: [] diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/DefaultJdkTrustConfig.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/DefaultJdkTrustConfig.java index 1ed506a8813a3..3e96fb041d280 100644 --- a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/DefaultJdkTrustConfig.java +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/DefaultJdkTrustConfig.java @@ -31,6 +31,7 @@ public final class DefaultJdkTrustConfig implements SslTrustConfig { private final BiFunction 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. @@ -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); } @@ -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"); @@ -106,6 +114,15 @@ public Collection getConfiguredCertificates() { return List.of(); } + /** + * @return the certificates found in the JVM default trust store. + */ + public Collection 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"; diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslConfiguration.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslConfiguration.java index 610fb444e0a93..236546b7c9b9b 100644 --- a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslConfiguration.java +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslConfiguration.java @@ -115,6 +115,19 @@ public Collection 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 getDefaultCertificates() { + List 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 diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslConfigurationLoader.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslConfigurationLoader.java index f0f1bbfd9ea72..e6559d5638e67 100644 --- a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslConfigurationLoader.java +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslConfigurationLoader.java @@ -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; diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/ssl.certificates.json b/rest-api-spec/src/main/resources/rest-api-spec/api/ssl.certificates.json index 233bc0882a87f..d13a264fe6b9f 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/ssl.certificates.json +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/ssl.certificates.json @@ -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)" + } + } } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SSLService.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SSLService.java index 9704335776f11..f996ccc93b94c 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SSLService.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SSLService.java @@ -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; @@ -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 excludes * 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. @@ -706,6 +707,30 @@ public Collection 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, includes + * 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 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 diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/action/GetCertificateInfoAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/action/GetCertificateInfoAction.java index 985bab5a0d1d9..a9dad73035596 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/action/GetCertificateInfoAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/action/GetCertificateInfoAction.java @@ -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; @@ -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 @@ -85,11 +92,4 @@ public void writeTo(StreamOutput out) throws IOException { } } - - public static class RequestBuilder extends ActionRequestBuilder { - public RequestBuilder(ElasticsearchClient client) { - super(client, GetCertificateInfoAction.INSTANCE, new Request()); - } - } - } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/action/TransportGetCertificateInfoAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/action/TransportGetCertificateInfoAction.java index 90a6397e90b54..168476fd398e6 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/action/TransportGetCertificateInfoAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/action/TransportGetCertificateInfoAction.java @@ -45,7 +45,9 @@ protected void doExecute( ActionListener listener ) { try { - Collection certificates = sslService.getLoadedCertificates(); + Collection certificates = request.includeDefaults() + ? sslService.getAllCertificates() + : sslService.getLoadedCertificates(); listener.onResponse(new GetCertificateInfoAction.Response(certificates)); } catch (GeneralSecurityException | IOException e) { listener.onFailure(e); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/rest/RestGetCertificateInfoAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/rest/RestGetCertificateInfoAction.java index 38e59a2e34df8..817acf0192d28 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/rest/RestGetCertificateInfoAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/rest/RestGetCertificateInfoAction.java @@ -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)); + } } - }); + ); } } diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/ssl/10_basic.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/ssl/10_basic.yml index 5717ec5824eb2..aa525527297b7 100644 --- a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/ssl/10_basic.yml +++ b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/ssl/10_basic.yml @@ -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 + diff --git a/x-pack/qa/xpack-prefix-rest-compat/src/yamlRestTestV7Compat/resources/rest-api-spec/api/xpack-ssl.certificates.json b/x-pack/qa/xpack-prefix-rest-compat/src/yamlRestTestV7Compat/resources/rest-api-spec/api/xpack-ssl.certificates.json index 7d25b0bf8f4f3..25f4a1bf33257 100644 --- a/x-pack/qa/xpack-prefix-rest-compat/src/yamlRestTestV7Compat/resources/rest-api-spec/api/xpack-ssl.certificates.json +++ b/x-pack/qa/xpack-prefix-rest-compat/src/yamlRestTestV7Compat/resources/rest-api-spec/api/xpack-ssl.certificates.json @@ -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)" + } + } } }