From 703d40f7111a2aac5095f6c06f1e653f6e47e402 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 09:29:15 -0400 Subject: [PATCH 01/16] Bump org.junit.jupiter:junit-jupiter from 5.11.1 to 5.11.2 (#4810) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 96b117b0c7..f96fa592f8 100644 --- a/build.gradle +++ b/build.gradle @@ -680,8 +680,8 @@ dependencies { testImplementation 'commons-validator:commons-validator:1.9.0' testImplementation 'org.springframework.kafka:spring-kafka-test:2.9.13' testImplementation "org.springframework:spring-beans:${spring_version}" - testImplementation 'org.junit.jupiter:junit-jupiter:5.11.1' - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.11.1' + testImplementation 'org.junit.jupiter:junit-jupiter:5.11.2' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.11.2' testImplementation('org.awaitility:awaitility:4.2.2') { exclude(group: 'org.hamcrest', module: 'hamcrest') } From 1b24f8ef43d9af030ebba7a4a9c8e1f52d4f1220 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 19:43:26 -0400 Subject: [PATCH 02/16] Bump org.checkerframework:checker-qual from 3.47.0 to 3.48.1 (#4808) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index f96fa592f8..350f0c8fbe 100644 --- a/build.gradle +++ b/build.gradle @@ -496,7 +496,7 @@ configurations { force "org.apache.httpcomponents:httpclient:4.5.14" force "org.apache.httpcomponents:httpcore:4.4.16" force "com.google.errorprone:error_prone_annotations:2.33.0" - force "org.checkerframework:checker-qual:3.47.0" + force "org.checkerframework:checker-qual:3.48.1" force "ch.qos.logback:logback-classic:1.5.10" force "commons-io:commons-io:2.17.0" } @@ -647,7 +647,7 @@ dependencies { runtimeOnly 'org.apache.ws.xmlschema:xmlschema-core:2.3.1' runtimeOnly 'org.apache.santuario:xmlsec:2.3.4' runtimeOnly "com.github.luben:zstd-jni:${versions.zstd}" - runtimeOnly 'org.checkerframework:checker-qual:3.47.0' + runtimeOnly 'org.checkerframework:checker-qual:3.48.1' runtimeOnly "org.bouncycastle:bcpkix-jdk18on:${versions.bouncycastle}" runtimeOnly 'org.scala-lang.modules:scala-java8-compat_3:1.0.2' From a1816fb020867b2af6732d3915115af7771b0fb9 Mon Sep 17 00:00:00 2001 From: Andriy Redko Date: Tue, 15 Oct 2024 14:45:47 -0400 Subject: [PATCH 03/16] Fix 'integTest' not called with test workflows during release (#4814) Signed-off-by: Andriy Redko --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 350f0c8fbe..57a79ae3cd 100644 --- a/build.gradle +++ b/build.gradle @@ -564,6 +564,7 @@ task integrationTest(type: Test) { } } +tasks.integTest.dependsOn(integrationTest) tasks.integrationTest.finalizedBy(jacocoTestReport) // report is always generated after integration tests run //run the integrationTest task before the check task From 2ab9eaf182f673d11660fb8121c8bc0b42e16e30 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Wed, 16 Oct 2024 15:04:09 -0400 Subject: [PATCH 04/16] Fix ChannelAlreadyClosed error in RateLimitersApiActionTest.testInvalidDeleteScenarios (#4812) Signed-off-by: Craig Perkins --- .../dlic/rest/api/RateLimitersApiAction.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/RateLimitersApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/RateLimitersApiAction.java index 7ef5c59c1e..387fe75cde 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/RateLimitersApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/RateLimitersApiAction.java @@ -179,13 +179,14 @@ private void authFailureConfigApiRequestHandlers(RequestHandler.RequestHandlersB // Try to remove the listener by name if (config.dynamic.auth_failure_listeners.getListeners().remove(listenerName) == null) { notFound(channel, "listener not found"); + } else { + saveOrUpdateConfiguration(client, configuration, new OnSucessActionListener<>(channel) { + @Override + public void onResponse(IndexResponse indexResponse) { + ok(channel, authFailureContent(config)); + } + }); } - saveOrUpdateConfiguration(client, configuration, new OnSucessActionListener<>(channel) { - @Override - public void onResponse(IndexResponse indexResponse) { - ok(channel, authFailureContent(config)); - } - }); }).error((status, toXContent) -> response(channel, status, toXContent))) .override(PUT, (channel, request, client) -> loadConfiguration(getConfigType(), false, false).valid(configuration -> { ConfigV7 config = (ConfigV7) configuration.getCEntry(CType.CONFIG.toLCString()); From 7692d77d551c7f9d0115b1466d1f4dd894b99324 Mon Sep 17 00:00:00 2001 From: Nils Bandener <33570290+nibix@users.noreply.github.com> Date: Thu, 17 Oct 2024 01:38:56 +0200 Subject: [PATCH 05/16] Fixed bulk index requests in BWC tests and hardened assertions (#4817) --- .../bwc/SecurityBackwardsCompatibilityIT.java | 32 +++++++++++++++++-- .../org/opensearch/security/bwc/Song.java | 8 ++++- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/bwc-test/src/test/java/org/opensearch/security/bwc/SecurityBackwardsCompatibilityIT.java b/bwc-test/src/test/java/org/opensearch/security/bwc/SecurityBackwardsCompatibilityIT.java index 6767a43ddd..89000c0816 100644 --- a/bwc-test/src/test/java/org/opensearch/security/bwc/SecurityBackwardsCompatibilityIT.java +++ b/bwc-test/src/test/java/org/opensearch/security/bwc/SecurityBackwardsCompatibilityIT.java @@ -27,6 +27,7 @@ import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.io.entity.StringEntity; import org.apache.hc.core5.http.message.BasicHeader; import org.apache.hc.core5.http.nio.ssl.TlsStrategy; import org.apache.hc.core5.reactor.ssl.TlsDetails; @@ -44,9 +45,11 @@ import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.common.util.io.IOUtils; +import org.opensearch.common.xcontent.support.XContentMapValues; import org.opensearch.security.bwc.helper.RestHelper; import org.opensearch.test.rest.OpenSearchRestTestCase; +import static org.apache.hc.core5.http.ContentType.APPLICATION_NDJSON; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.equalTo; @@ -239,15 +242,21 @@ private void ingestData(String index) throws IOException { } }); bulkRequestBody.append(objectMapper.writeValueAsString(indexRequest) + "\n"); - bulkRequestBody.append(objectMapper.writeValueAsString(Song.randomSong().asJson()) + "\n"); + bulkRequestBody.append(Song.randomSong().asJson() + "\n"); } List responses = RestHelper.requestAgainstAllNodes( testUserRestClient, "POST", "_bulk?refresh=wait_for", - RestHelper.toHttpEntity(bulkRequestBody.toString()) + new StringEntity(bulkRequestBody.toString(), APPLICATION_NDJSON) ); responses.forEach(r -> assertThat(r.getStatusLine().getStatusCode(), is(200))); + for (Response response : responses) { + Map responseMap = responseAsMap(response); + List itemResults = (List) XContentMapValues.extractValue(responseMap, "items", "index", "result"); + assertTrue("More than 0 response items", itemResults.size() > 0); + assertTrue("All results are 'created': " + itemResults, itemResults.stream().allMatch(i -> i.equals("created"))); + } } } @@ -266,6 +275,25 @@ private void searchMatchAll(String index) throws IOException { RestHelper.toHttpEntity(matchAllQuery) ); responses.forEach(r -> assertThat(r.getStatusLine().getStatusCode(), is(200))); + + for (Response response : responses) { + Map responseMap = responseAsMap(response); + @SuppressWarnings("unchecked") + List> sourceDocs = (List>) XContentMapValues.extractValue(responseMap, "hits", "hits", "_source"); + + for (Map sourceDoc : sourceDocs) { + assertNull("response doc should not contain field forbidden by FLS: " + responseMap, sourceDoc.get(Song.FIELD_LYRICS)); + assertNotNull( + "response doc should contain field not forbidden by FLS: " + responseMap, + sourceDoc.get(Song.FIELD_ARTIST) + ); + assertEquals( + "response doc should always have genre rock: " + responseMap, + Song.GENRE_ROCK, + sourceDoc.get(Song.FIELD_GENRE) + ); + } + } } } diff --git a/bwc-test/src/test/java/org/opensearch/security/bwc/Song.java b/bwc-test/src/test/java/org/opensearch/security/bwc/Song.java index f60d5f0fcb..ff7b8d9f87 100644 --- a/bwc-test/src/test/java/org/opensearch/security/bwc/Song.java +++ b/bwc-test/src/test/java/org/opensearch/security/bwc/Song.java @@ -52,6 +52,8 @@ public class Song { public static final String GENRE_JAZZ = "jazz"; public static final String GENRE_BLUES = "blues"; + public static final String[] GENRES = new String[] { GENRE_BLUES, GENRE_JAZZ, GENRE_ROCK }; + public static final String QUERY_TITLE_NEXT_SONG = FIELD_TITLE + ":" + "\"" + TITLE_NEXT_SONG + "\""; public static final String QUERY_TITLE_POISON = FIELD_TITLE + ":" + TITLE_POISON; public static final String QUERY_TITLE_MAGNUM_OPUS = FIELD_TITLE + ":" + TITLE_MAGNUM_OPUS; @@ -112,7 +114,11 @@ public static Song randomSong() { UUID.randomUUID().toString(), UUID.randomUUID().toString(), Randomness.get().nextInt(5), - UUID.randomUUID().toString() + randomGenre() ); } + + static String randomGenre() { + return GENRES[Randomness.get().nextInt(GENRES.length)]; + } } From e0fb8fcb817ac389e8176a9408438b009aa5ef9f Mon Sep 17 00:00:00 2001 From: Muneer Kolarkunnu <33829651+akolarkunnu@users.noreply.github.com> Date: Fri, 18 Oct 2024 18:25:58 +0530 Subject: [PATCH 06/16] Improve error message when a node with an incorrectly configured certificate attempts to connect (#4818) Signed-off-by: Abdul Muneer Kolarkunnu --- .../opensearch/security/ssl/util/ExceptionUtils.java | 10 ++++++++-- .../security/transport/SecurityRequestHandler.java | 2 +- .../security/ccstest/CrossClusterSearchTests.java | 9 ++++++++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/opensearch/security/ssl/util/ExceptionUtils.java b/src/main/java/org/opensearch/security/ssl/util/ExceptionUtils.java index 83982239f0..4683075f1d 100644 --- a/src/main/java/org/opensearch/security/ssl/util/ExceptionUtils.java +++ b/src/main/java/org/opensearch/security/ssl/util/ExceptionUtils.java @@ -76,7 +76,13 @@ public static OpenSearchException createJwkCreationException(Throwable cause) { return new OpenSearchException("An error occurred during the creation of Jwk: {}", cause, cause.getMessage()); } - public static OpenSearchException createTransportClientNoLongerSupportedException() { - return new OpenSearchException("Transport client authentication no longer supported."); + public static OpenSearchException clusterWrongNodeCertConfigException(String sslPrincipal) { + return new OpenSearchException( + "Node presenting certificate with SSL Principal {" + + sslPrincipal + + "} could" + + " not securely connect to the cluster. Please ensure the principal is correct and present in the" + + " nodes_dn list." + ); } } diff --git a/src/main/java/org/opensearch/security/transport/SecurityRequestHandler.java b/src/main/java/org/opensearch/security/transport/SecurityRequestHandler.java index 5845c63672..18c0c21282 100644 --- a/src/main/java/org/opensearch/security/transport/SecurityRequestHandler.java +++ b/src/main/java/org/opensearch/security/transport/SecurityRequestHandler.java @@ -290,7 +290,7 @@ protected void messageReceivedDecorate( || HeaderHelper.isTrustedClusterRequest(getThreadContext()) || HeaderHelper.isExtensionRequest(getThreadContext()))) { // CS-ENFORCE-SINGLE - final OpenSearchException exception = ExceptionUtils.createTransportClientNoLongerSupportedException(); + final OpenSearchException exception = ExceptionUtils.clusterWrongNodeCertConfigException(principal); log.error(exception.toString()); transportChannel.sendResponse(exception); return; diff --git a/src/test/java/org/opensearch/security/ccstest/CrossClusterSearchTests.java b/src/test/java/org/opensearch/security/ccstest/CrossClusterSearchTests.java index 32ab78dbdf..d6a427e581 100644 --- a/src/test/java/org/opensearch/security/ccstest/CrossClusterSearchTests.java +++ b/src/test/java/org/opensearch/security/ccstest/CrossClusterSearchTests.java @@ -1355,7 +1355,14 @@ public void testCcsWithDiffCertsWithNoNodesDnUpdate() throws Exception { String uri = "cross_cluster_two:twitter/_search?pretty"; HttpResponse ccs = rh1.executeGetRequest(uri, encodeBasicHeader("twitter", "nagilum")); assertThat(ccs.getStatusCode(), equalTo(HttpStatus.SC_INTERNAL_SERVER_ERROR)); - assertThat(ccs.getBody(), containsString("Transport client authentication no longer supported")); + assertThat( + ccs.getBody(), + containsString( + "Node presenting certificate with SSL Principal " + + "{CN=node-0.example.com,OU=SSL,O=Test,L=Test,C=DE} could not securely connect to the cluster. Please" + + " ensure the principal is correct and present in the nodes_dn list." + ) + ); } @Test From f81faed831eee4acc289eb6b4b61b754405d011a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Oct 2024 09:55:27 -0400 Subject: [PATCH 07/16] Bump com.google.errorprone:error_prone_annotations from 2.33.0 to 2.34.0 (#4822) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 57a79ae3cd..dde93178d1 100644 --- a/build.gradle +++ b/build.gradle @@ -495,7 +495,7 @@ configurations { // For integrationTest force "org.apache.httpcomponents:httpclient:4.5.14" force "org.apache.httpcomponents:httpcore:4.4.16" - force "com.google.errorprone:error_prone_annotations:2.33.0" + force "com.google.errorprone:error_prone_annotations:2.34.0" force "org.checkerframework:checker-qual:3.48.1" force "ch.qos.logback:logback-classic:1.5.10" force "commons-io:commons-io:2.17.0" @@ -604,7 +604,7 @@ dependencies { runtimeOnly 'com.eclipsesource.minimal-json:minimal-json:0.9.5' runtimeOnly 'commons-codec:commons-codec:1.17.1' runtimeOnly 'org.cryptacular:cryptacular:1.2.7' - compileOnly 'com.google.errorprone:error_prone_annotations:2.33.0' + compileOnly 'com.google.errorprone:error_prone_annotations:2.34.0' runtimeOnly 'com.sun.istack:istack-commons-runtime:4.2.0' runtimeOnly 'jakarta.xml.bind:jakarta.xml.bind-api:4.0.2' runtimeOnly 'org.ow2.asm:asm:9.7.1' From c11a8911905afbe0a1a743c87a84fb0852d35284 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Oct 2024 09:55:54 -0400 Subject: [PATCH 08/16] Bump org.passay:passay from 1.6.5 to 1.6.6 (#4821) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index dde93178d1..75fd1fdb92 100644 --- a/build.gradle +++ b/build.gradle @@ -594,7 +594,7 @@ dependencies { implementation 'org.apache.commons:commons-collections4:4.4' //Password generation - implementation 'org.passay:passay:1.6.5' + implementation 'org.passay:passay:1.6.6' implementation "org.apache.kafka:kafka-clients:${kafka_version}" From 1b207a8332d084420475c6912310cbf825c72495 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Oct 2024 10:44:05 -0400 Subject: [PATCH 09/16] Bump ch.qos.logback:logback-classic from 1.5.10 to 1.5.11 (#4823) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 75fd1fdb92..db494876ca 100644 --- a/build.gradle +++ b/build.gradle @@ -497,7 +497,7 @@ configurations { force "org.apache.httpcomponents:httpcore:4.4.16" force "com.google.errorprone:error_prone_annotations:2.34.0" force "org.checkerframework:checker-qual:3.48.1" - force "ch.qos.logback:logback-classic:1.5.10" + force "ch.qos.logback:logback-classic:1.5.11" force "commons-io:commons-io:2.17.0" } } From 8d7259db2417727628ffa091189c70bad4d5690a Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Mon, 21 Oct 2024 12:27:29 -0400 Subject: [PATCH 10/16] Fix issue in HashingStoredFieldVisitor with stored fields (#4826) Signed-off-by: Craig Perkins --- .../security/StoredFieldsTests.java | 110 ++++++++++++++++++ .../configuration/DlsFlsFilterLeafReader.java | 18 ++- 2 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 src/integrationTest/java/org/opensearch/security/StoredFieldsTests.java diff --git a/src/integrationTest/java/org/opensearch/security/StoredFieldsTests.java b/src/integrationTest/java/org/opensearch/security/StoredFieldsTests.java new file mode 100644 index 0000000000..9bcc0c5526 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/StoredFieldsTests.java @@ -0,0 +1,110 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + */ +package org.opensearch.security; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.apache.http.HttpStatus; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.action.admin.indices.create.CreateIndexResponse; +import org.opensearch.client.Client; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; + +import static org.opensearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; + +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class StoredFieldsTests { + static final TestSecurityConfig.User TEST_USER_MASKED_FIELDS = new TestSecurityConfig.User("test_user_masked_fields").roles( + new TestSecurityConfig.Role("role_masked_fields").clusterPermissions("cluster_composite_ops_ro") + .indexPermissions("read") + .maskedFields("restricted") + .on("test_index") + ); + + static final TestSecurityConfig.User TEST_USER_FLS = new TestSecurityConfig.User("test_user_fls").roles( + new TestSecurityConfig.Role("role_fls").clusterPermissions("cluster_composite_ops_ro") + .indexPermissions("read") + .fls("~restricted") + .on("test_index") + ); + + @ClassRule + public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(TEST_USER_MASKED_FIELDS, TEST_USER_FLS) + .build(); + + @BeforeClass + public static void createTestData() { + try (Client client = cluster.getInternalNodeClient()) { + CreateIndexResponse r = client.admin() + .indices() + .prepareCreate("test_index") + .setMapping("raw", "type=keyword,store=true", "restricted", "type=keyword,store=true") + .get(); + + client.prepareIndex("test_index").setRefreshPolicy(IMMEDIATE).setSource("raw", "hello", "restricted", "boo!").get(); + } + } + + @Test + public void testStoredWithWithApplicableMaskedFieldRestrictions() { + try (TestRestClient client = cluster.getRestClient(TEST_USER_MASKED_FIELDS)) { + TestRestClient.HttpResponse normalSearchResponse = client.get("test_index/_search"); + Assert.assertFalse(normalSearchResponse.getBody().contains("boo!")); + + TestRestClient.HttpResponse fieldSearchResponse = client.postJson("test_index/_search", """ + { + "stored_fields": [ + "raw", + "restricted" + ] + } + """); + fieldSearchResponse.assertStatusCode(HttpStatus.SC_OK); + Assert.assertTrue(fieldSearchResponse.getBody().contains("raw")); + Assert.assertTrue(fieldSearchResponse.getBody().contains("hello")); + Assert.assertTrue(fieldSearchResponse.getBody().contains("restricted")); + Assert.assertFalse(fieldSearchResponse.getBody().contains("boo!")); + } + } + + @Test + public void testStoredWithWithApplicableFlsRestrictions() { + try (TestRestClient client = cluster.getRestClient(TEST_USER_FLS)) { + TestRestClient.HttpResponse normalSearchResponse = client.get("test_index/_search"); + Assert.assertFalse(normalSearchResponse.getBody().contains("boo!")); + + TestRestClient.HttpResponse fieldSearchResponse = client.postJson("test_index/_search", """ + { + "stored_fields": [ + "raw", + "restricted" + ] + } + """); + fieldSearchResponse.assertStatusCode(HttpStatus.SC_OK); + Assert.assertTrue(fieldSearchResponse.getBody().contains("raw")); + Assert.assertTrue(fieldSearchResponse.getBody().contains("hello")); + Assert.assertFalse(fieldSearchResponse.getBody().contains("restricted")); + Assert.assertFalse(fieldSearchResponse.getBody().contains("boo!")); + } + } + +} diff --git a/src/main/java/org/opensearch/security/configuration/DlsFlsFilterLeafReader.java b/src/main/java/org/opensearch/security/configuration/DlsFlsFilterLeafReader.java index b09745727f..b85542393b 100644 --- a/src/main/java/org/opensearch/security/configuration/DlsFlsFilterLeafReader.java +++ b/src/main/java/org/opensearch/security/configuration/DlsFlsFilterLeafReader.java @@ -663,11 +663,20 @@ public void binaryField(final FieldInfo fieldInfo, final byte[] value) throws IO } delegate.binaryField(fieldInfo, Utils.jsonMapToByteArray(filteredSource)); - } else { + } else if (shouldInclude(fieldInfo.name)) { delegate.binaryField(fieldInfo, value); } } + private boolean shouldInclude(String field) { + if (excludesSet != null && !excludesSet.isEmpty()) { + return !excludesSet.contains(field); + } else if (includesSet != null && !includesSet.isEmpty()) { + return includesSet.contains(field); + } + return true; + } + @Override public Status needsField(final FieldInfo fieldInfo) throws IOException { return isFls(fieldInfo.name) ? delegate.needsField(fieldInfo) : Status.NO; @@ -733,7 +742,12 @@ public void binaryField(final FieldInfo fieldInfo, final byte[] value) throws IO final XContentBuilder xBuilder = XContentBuilder.builder(bytesRefTuple.v1().xContent()).map(filteredSource); delegate.binaryField(fieldInfo, BytesReference.toBytes(BytesReference.bytes(xBuilder))); } else { - delegate.binaryField(fieldInfo, value); + final MaskedField mf = maskedFieldsMap.getMaskedField(fieldInfo.name).orElse(null); + if (mf != null) { + delegate.binaryField(fieldInfo, mf.mask(value)); + } else { + delegate.binaryField(fieldInfo, value); + } } } From 3edfac840ea76752388301107984dc456e7122a3 Mon Sep 17 00:00:00 2001 From: Andrey Pleskach Date: Mon, 21 Oct 2024 19:50:07 +0200 Subject: [PATCH 11/16] Bump gradle to 8.10.2 (#4828) Signed-off-by: Andrey Pleskach --- gradle/wrapper/gradle-wrapper.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2b189974c2..fb602ee2af 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=5b9c5eb3f9fc2c94abaea57d90bd78747ca117ddbbf96c859d3741181a12bf2a -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip +distributionSha256Sum=31c55713e40233a8303827ceb42ca48a47267a0ad4bab9177123121e71524c26 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From 811f26de2444b6db41309d8d1c5bcc5debe6ec0e Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Mon, 21 Oct 2024 13:51:02 -0400 Subject: [PATCH 12/16] Ensure that dual mode enabled flag from cluster settings can get propagated to core (#4820) Signed-off-by: Craig Perkins --- .../EncryptionInTransitMigrationTests.java | 70 +++++++++++++++++++ .../test/framework/cluster/LocalCluster.java | 46 +++++++++++- .../cluster/LocalOpenSearchCluster.java | 12 +++- ...inimumSecuritySettingsSupplierFactory.java | 12 ++++ .../security/OpenSearchSecurityPlugin.java | 4 +- .../ssl/OpenSearchSecureSettingsFactory.java | 16 ++++- .../ssl/OpenSearchSecuritySSLPlugin.java | 4 +- 7 files changed, 156 insertions(+), 8 deletions(-) create mode 100644 src/integrationTest/java/org/opensearch/security/EncryptionInTransitMigrationTests.java diff --git a/src/integrationTest/java/org/opensearch/security/EncryptionInTransitMigrationTests.java b/src/integrationTest/java/org/opensearch/security/EncryptionInTransitMigrationTests.java new file mode 100644 index 0000000000..58eb7218e6 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/EncryptionInTransitMigrationTests.java @@ -0,0 +1,70 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + */ +package org.opensearch.security; + +import java.util.Map; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.awaitility.Awaitility.await; +import static org.junit.Assert.assertEquals; + +/** + * Test related to SSL-only mode of security plugin. In this mode, the security plugin is responsible only for TLS/SSL encryption. + * Therefore, the plugin does not perform authentication and authorization. Moreover, the REST resources (e.g. /_plugins/_security/whoami, + * /_plugins/_security/authinfo, etc.) provided by the plugin are not available. + */ +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class EncryptionInTransitMigrationTests { + + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.DEFAULT) + .anonymousAuth(false) + .loadConfigurationIntoIndex(false) + .nodeSettings(Map.of(ConfigConstants.SECURITY_SSL_ONLY, true)) + .sslOnly(true) + .nodeSpecificSettings(0, Map.of(ConfigConstants.SECURITY_CONFIG_SSL_DUAL_MODE_ENABLED, true)) + .nodeSpecificSettings(1, Map.of(ConfigConstants.SECURITY_CONFIG_SSL_DUAL_MODE_ENABLED, true)) + .extectedNodeStartupCount(2) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .build(); + + @Test + public void shouldOnlyConnectWithThirdNodeAfterDynamicDualModeChange() { + try (TestRestClient client = cluster.getRestClient()) { + TestRestClient.HttpResponse response = client.get("_cat/nodes"); + response.assertStatusCode(200); + + String[] lines = response.getBody().split("\n"); + assertEquals("Expected 2 nodes in the initial response", 2, lines.length); + + String settingsJson = "{\"persistent\": {\"plugins.security_config.ssl_dual_mode_enabled\": false}}"; + TestRestClient.HttpResponse settingsResponse = client.putJson("_cluster/settings", settingsJson); + settingsResponse.assertStatusCode(200); + + await().atMost(10, SECONDS).pollInterval(1, SECONDS).until(() -> { + TestRestClient.HttpResponse secondResponse = client.get("_cat/nodes"); + String[] secondLines = secondResponse.getBody().split("\n"); + return secondLines.length == 3; + }); + } + } +} diff --git a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java index 894bb5baa9..d2c53c1de7 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java @@ -85,7 +85,9 @@ public class LocalCluster extends ExternalResource implements AutoCloseable, Ope private final List> plugins; private final ClusterManager clusterManager; private final TestSecurityConfig testSecurityConfig; + private Map nodeSpecificOverride; private Settings nodeOverride; + private Integer expectedNodeStartupCount; private final String clusterName; private final MinimumSecuritySettingsSupplierFactory minimumOpenSearchSettingsSupplierFactory; private final TestCertificates testCertificates; @@ -100,6 +102,7 @@ private LocalCluster( String clusterName, TestSecurityConfig testSgConfig, boolean sslOnly, + Map nodeSpecificOverride, Settings nodeOverride, ClusterManager clusterManager, List> plugins, @@ -108,13 +111,15 @@ private LocalCluster( Map remotes, List testIndices, boolean loadConfigurationIntoIndex, - String defaultConfigurationInitDirectory + String defaultConfigurationInitDirectory, + Integer expectedNodeStartupCount ) { this.plugins = plugins; this.testCertificates = testCertificates; this.clusterManager = clusterManager; this.testSecurityConfig = testSgConfig; this.sslOnly = sslOnly; + this.nodeSpecificOverride = nodeSpecificOverride; this.nodeOverride = nodeOverride; this.clusterName = clusterName; this.minimumOpenSearchSettingsSupplierFactory = new MinimumSecuritySettingsSupplierFactory(testCertificates); @@ -125,6 +130,7 @@ private LocalCluster( if (StringUtils.isNoneBlank(defaultConfigurationInitDirectory)) { System.setProperty(INIT_CONFIGURATION_DIR, defaultConfigurationInitDirectory); } + this.expectedNodeStartupCount = expectedNodeStartupCount; } public String getSnapshotDirPath() { @@ -232,6 +238,7 @@ private void start() { try { NodeSettingsSupplier nodeSettingsSupplier = minimumOpenSearchSettingsSupplierFactory.minimumOpenSearchSettings( sslOnly, + nodeSpecificOverride, nodeOverride ); localOpenSearchCluster = new LocalOpenSearchCluster( @@ -239,7 +246,8 @@ private void start() { clusterManager, nodeSettingsSupplier, plugins, - testCertificates + testCertificates, + expectedNodeStartupCount ); localOpenSearchCluster.start(); @@ -312,8 +320,10 @@ public CertificateData getAdminCertificate() { public static class Builder { private final Settings.Builder nodeOverrideSettingsBuilder = Settings.builder(); + private final Map nodeSpecificOverrideSettingsBuilder = new HashMap<>(); private boolean sslOnly = false; + private Integer expectedNodeStartupCount; private final List> plugins = new ArrayList<>(); private Map remoteClusters = new HashMap<>(); private List clusterDependencies = new ArrayList<>(); @@ -365,6 +375,11 @@ public Builder sslOnly(boolean sslOnly) { return this; } + public Builder extectedNodeStartupCount(int expectedNodeStartupCount) { + this.expectedNodeStartupCount = expectedNodeStartupCount; + return this; + } + public Builder nodeSettings(Map settings) { settings.forEach((key, value) -> { if (value instanceof List) { @@ -378,6 +393,25 @@ public Builder nodeSettings(Map settings) { return this; } + public Builder nodeSpecificSettings(int nodeNumber, Map settings) { + if (!nodeSpecificOverrideSettingsBuilder.containsKey(nodeNumber)) { + Settings.Builder builderCopy = Settings.builder(); + builderCopy.put(nodeOverrideSettingsBuilder.build()); + nodeSpecificOverrideSettingsBuilder.put(nodeNumber, builderCopy); + } + Settings.Builder nodeSettingsBuilder = nodeSpecificOverrideSettingsBuilder.get(nodeNumber); + settings.forEach((key, value) -> { + if (value instanceof List) { + List values = ((List) value).stream().map(String::valueOf).collect(Collectors.toList()); + nodeSettingsBuilder.putList(key, values); + } else { + nodeSettingsBuilder.put(key, String.valueOf(value)); + } + }); + + return this; + } + /** * Adds additional plugins to the cluster */ @@ -512,10 +546,15 @@ public LocalCluster build() { } clusterName += "_" + num.incrementAndGet(); Settings settings = nodeOverrideSettingsBuilder.build(); + Map nodeSpecificSettings = new HashMap<>(); + for (Map.Entry entry : nodeSpecificOverrideSettingsBuilder.entrySet()) { + nodeSpecificSettings.put(entry.getKey(), entry.getValue().build()); + } return new LocalCluster( clusterName, testSecurityConfig, sslOnly, + nodeSpecificSettings, settings, clusterManager, plugins, @@ -524,7 +563,8 @@ public LocalCluster build() { remoteClusters, testIndices, loadConfigurationIntoIndex, - defaultConfigurationInitDirectory + defaultConfigurationInitDirectory, + expectedNodeStartupCount ); } catch (Exception e) { log.error("Failed to build LocalCluster", e); diff --git a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java index 96da63d9fb..8570c3d398 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java @@ -97,6 +97,7 @@ public class LocalOpenSearchCluster { private final List> additionalPlugins; private final List nodes = new ArrayList<>(); private final TestCertificates testCertificates; + private final Integer expectedNodeStartupCount; private File clusterHomeDir; private List seedHosts; @@ -112,13 +113,15 @@ public LocalOpenSearchCluster( ClusterManager clusterManager, NodeSettingsSupplier nodeSettingsSupplier, List> additionalPlugins, - TestCertificates testCertificates + TestCertificates testCertificates, + Integer expectedNodeStartCount ) { this.clusterName = clusterName; this.clusterManager = clusterManager; this.nodeSettingsSupplier = nodeSettingsSupplier; this.additionalPlugins = additionalPlugins; this.testCertificates = testCertificates; + this.expectedNodeStartupCount = expectedNodeStartCount; try { createClusterDirectory(clusterName); } catch (IOException e) { @@ -198,7 +201,12 @@ public void start() throws Exception { log.info("Startup finished. Waiting for GREEN"); - waitForCluster(ClusterHealthStatus.GREEN, TimeValue.timeValueSeconds(10), nodes.size()); + int expectedCount = nodes.size(); + if (expectedNodeStartupCount != null) { + expectedCount = expectedNodeStartupCount; + } + + waitForCluster(ClusterHealthStatus.GREEN, TimeValue.timeValueSeconds(10), expectedCount); log.info("Started: {}", this); } diff --git a/src/integrationTest/java/org/opensearch/test/framework/cluster/MinimumSecuritySettingsSupplierFactory.java b/src/integrationTest/java/org/opensearch/test/framework/cluster/MinimumSecuritySettingsSupplierFactory.java index 4ad5f8420e..34a105ea39 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/MinimumSecuritySettingsSupplierFactory.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/MinimumSecuritySettingsSupplierFactory.java @@ -28,6 +28,8 @@ package org.opensearch.test.framework.cluster; +import java.util.Map; + import org.opensearch.common.settings.Settings; import org.opensearch.security.support.ConfigConstants; import org.opensearch.test.framework.certificate.TestCertificates; @@ -51,6 +53,16 @@ public NodeSettingsSupplier minimumOpenSearchSettings(boolean sslOnly, Settings return i -> minimumOpenSearchSettingsBuilder(i, sslOnly).put(other).build(); } + public NodeSettingsSupplier minimumOpenSearchSettings(boolean sslOnly, Map nodeOverride, Settings other) { + return i -> { + Settings override = nodeOverride.get(i); + if (override != null) { + return minimumOpenSearchSettingsBuilder(i, sslOnly).put(other).put(override).build(); + } + return minimumOpenSearchSettingsBuilder(i, sslOnly).put(other).build(); + }; + } + private Settings.Builder minimumOpenSearchSettingsBuilder(int node, boolean sslOnly) { Settings.Builder builder = Settings.builder(); diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 594af15f03..15d5e4c286 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -2166,7 +2166,9 @@ public PluginSubject getPluginSubject(Plugin plugin) { @Override public Optional getSecureSettingFactory(Settings settings) { - return Optional.of(new OpenSearchSecureSettingsFactory(threadPool, sks, evaluateSslExceptionHandler(), securityRestHandler)); + return Optional.of( + new OpenSearchSecureSettingsFactory(threadPool, sks, evaluateSslExceptionHandler(), securityRestHandler, SSLConfig) + ); } @SuppressWarnings("removal") diff --git a/src/main/java/org/opensearch/security/ssl/OpenSearchSecureSettingsFactory.java b/src/main/java/org/opensearch/security/ssl/OpenSearchSecureSettingsFactory.java index 5351eea57e..9d482b18a8 100644 --- a/src/main/java/org/opensearch/security/ssl/OpenSearchSecureSettingsFactory.java +++ b/src/main/java/org/opensearch/security/ssl/OpenSearchSecureSettingsFactory.java @@ -27,6 +27,7 @@ import org.opensearch.security.filter.SecurityRestFilter; import org.opensearch.security.ssl.http.netty.Netty4ConditionalDecompressor; import org.opensearch.security.ssl.http.netty.Netty4HttpRequestHeaderVerifier; +import org.opensearch.security.ssl.transport.SSLConfig; import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.Transport; import org.opensearch.transport.TransportAdapterProvider; @@ -38,17 +39,20 @@ public class OpenSearchSecureSettingsFactory implements SecureSettingsFactory { private final SecurityKeyStore sks; private final SslExceptionHandler sslExceptionHandler; private final SecurityRestFilter restFilter; + private final SSLConfig sslConfig; public OpenSearchSecureSettingsFactory( ThreadPool threadPool, SecurityKeyStore sks, SslExceptionHandler sslExceptionHandler, - SecurityRestFilter restFilter + SecurityRestFilter restFilter, + SSLConfig sslConfig ) { this.threadPool = threadPool; this.sks = sks; this.sslExceptionHandler = sslExceptionHandler; this.restFilter = restFilter; + this.sslConfig = sslConfig; } @Override @@ -64,6 +68,16 @@ public void onError(Throwable t) { }); } + @Override + public Optional parameters(Settings settings) { + return Optional.of(new SecureTransportParameters() { + @Override + public boolean dualModeEnabled() { + return sslConfig.isDualModeEnabled(); + } + }); + } + @Override public Optional buildSecureServerTransportEngine(Settings settings, Transport transport) throws SSLException { return Optional.of(sks.createServerTransportSSLEngine()); diff --git a/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java b/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java index e6a1b47888..c16706c870 100644 --- a/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java +++ b/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java @@ -674,7 +674,9 @@ public List getSettingsFilter() { @Override public Optional getSecureSettingFactory(Settings settings) { - return Optional.of(new OpenSearchSecureSettingsFactory(threadPool, sks, NOOP_SSL_EXCEPTION_HANDLER, securityRestHandler)); + return Optional.of( + new OpenSearchSecureSettingsFactory(threadPool, sks, NOOP_SSL_EXCEPTION_HANDLER, securityRestHandler, SSLConfig) + ); } protected Settings migrateSettings(Settings settings) { From db6e7dc1664694497d80ee0b8b727350c1086b47 Mon Sep 17 00:00:00 2001 From: Andrey Pleskach Date: Tue, 22 Oct 2024 18:25:26 +0200 Subject: [PATCH 13/16] Refactor SSL Configuration (#4671) --- build.gradle | 3 + checkstyle/checkstyle.xml | 7 + .../security/OpenSearchSecurityPlugin.java | 15 +- .../dlic/rest/api/SecurityRestApiActions.java | 12 +- .../rest/api/SecuritySSLCertsApiAction.java | 89 ++-- .../TransportCertificatesInfoNodesAction.java | 50 +- .../ssl/OpenSearchSecureSettingsFactory.java | 13 +- .../ssl/OpenSearchSecuritySSLPlugin.java | 14 +- .../security/ssl/SecureSSLSettings.java | 2 +- .../security/ssl/SslConfiguration.java | 148 ++++++ .../security/ssl/SslContextHandler.java | 165 +++++++ .../security/ssl/SslSettingsManager.java | 384 +++++++++++++++ .../security/ssl/config/CertType.java | 33 ++ .../security/ssl/config/Certificate.java | 188 +++++++ .../ssl/config/KeyStoreConfiguration.java | 201 ++++++++ .../security/ssl/config/KeyStoreUtils.java | 218 ++++++++ .../ssl/config/SslCertificatesLoader.java | 171 +++++++ .../security/ssl/config/SslParameters.java | 197 ++++++++ .../ssl/config/TrustStoreConfiguration.java | 185 +++++++ .../ssl/rest/SecuritySSLInfoAction.java | 43 +- .../security/ssl/util/SSLConfigConstants.java | 64 ++- .../security/ssl/CertificatesRule.java | 318 ++++++++++++ .../security/ssl/CertificatesUtils.java | 43 ++ .../ssl/OpenSearchSecuritySSLPluginTest.java | 20 +- .../org/opensearch/security/ssl/SSLTest.java | 2 +- .../SecuritySSLReloadCertsActionTests.java | 9 +- .../security/ssl/SslContextHandlerTest.java | 266 ++++++++++ .../security/ssl/SslSettingsManagerTest.java | 464 ++++++++++++++++++ .../security/ssl/config/CertificateTest.java | 38 ++ .../config/JdkSslCertificatesLoaderTest.java | 318 ++++++++++++ .../config/PemSslCertificatesLoaderTest.java | 174 +++++++ .../ssl/config/SslCertificatesLoaderTest.java | 66 +++ .../ssl/config/SslParametersTest.java | 90 ++++ 33 files changed, 3887 insertions(+), 123 deletions(-) create mode 100644 src/main/java/org/opensearch/security/ssl/SslConfiguration.java create mode 100644 src/main/java/org/opensearch/security/ssl/SslContextHandler.java create mode 100644 src/main/java/org/opensearch/security/ssl/SslSettingsManager.java create mode 100644 src/main/java/org/opensearch/security/ssl/config/CertType.java create mode 100644 src/main/java/org/opensearch/security/ssl/config/Certificate.java create mode 100644 src/main/java/org/opensearch/security/ssl/config/KeyStoreConfiguration.java create mode 100644 src/main/java/org/opensearch/security/ssl/config/KeyStoreUtils.java create mode 100644 src/main/java/org/opensearch/security/ssl/config/SslCertificatesLoader.java create mode 100644 src/main/java/org/opensearch/security/ssl/config/SslParameters.java create mode 100644 src/main/java/org/opensearch/security/ssl/config/TrustStoreConfiguration.java create mode 100644 src/test/java/org/opensearch/security/ssl/CertificatesRule.java create mode 100644 src/test/java/org/opensearch/security/ssl/CertificatesUtils.java create mode 100644 src/test/java/org/opensearch/security/ssl/SslContextHandlerTest.java create mode 100644 src/test/java/org/opensearch/security/ssl/SslSettingsManagerTest.java create mode 100644 src/test/java/org/opensearch/security/ssl/config/CertificateTest.java create mode 100644 src/test/java/org/opensearch/security/ssl/config/JdkSslCertificatesLoaderTest.java create mode 100644 src/test/java/org/opensearch/security/ssl/config/PemSslCertificatesLoaderTest.java create mode 100644 src/test/java/org/opensearch/security/ssl/config/SslCertificatesLoaderTest.java create mode 100644 src/test/java/org/opensearch/security/ssl/config/SslParametersTest.java diff --git a/build.gradle b/build.gradle index db494876ca..887966e6c7 100644 --- a/build.gradle +++ b/build.gradle @@ -686,6 +686,9 @@ dependencies { testImplementation('org.awaitility:awaitility:4.2.2') { exclude(group: 'org.hamcrest', module: 'hamcrest') } + testImplementation "org.bouncycastle:bcpkix-jdk18on:${versions.bouncycastle}" + testImplementation "org.bouncycastle:bcutil-jdk18on:${versions.bouncycastle}" + // Only osx-x86_64, osx-aarch_64, linux-x86_64, linux-aarch_64, windows-x86_64 are available if (osdetector.classifier in ["osx-x86_64", "osx-aarch_64", "linux-x86_64", "linux-aarch_64", "windows-x86_64"]) { testImplementation "io.netty:netty-tcnative-classes:2.0.61.Final" diff --git a/checkstyle/checkstyle.xml b/checkstyle/checkstyle.xml index 04a36c49c1..a9c1a8f765 100644 --- a/checkstyle/checkstyle.xml +++ b/checkstyle/checkstyle.xml @@ -43,6 +43,13 @@ + + + + + + + diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 15d5e4c286..9076b1da1f 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -648,7 +648,7 @@ public List getRestHandlers( evaluator, threadPool, Objects.requireNonNull(auditLog), - sks, + sslSettingsManager, Objects.requireNonNull(userService), sslCertReloadEnabled, passwordHasher @@ -1207,9 +1207,8 @@ public Collection createComponents( components.add(userService); components.add(passwordHasher); - if (!ExternalSecurityKeyStore.hasExternalSslContext(settings)) { - components.add(sks); - } + components.add(sslSettingsManager); + final var allowDefaultInit = settings.getAsBoolean(SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, false); final var useClusterState = useClusterStateToInitSecurityConfig(settings); if (!SSLConfig.isSslOnlyMode() && !isDisabled(settings) && allowDefaultInit && useClusterState) { @@ -2167,7 +2166,13 @@ public PluginSubject getPluginSubject(Plugin plugin) { @Override public Optional getSecureSettingFactory(Settings settings) { return Optional.of( - new OpenSearchSecureSettingsFactory(threadPool, sks, evaluateSslExceptionHandler(), securityRestHandler, SSLConfig) + new OpenSearchSecureSettingsFactory( + threadPool, + sslSettingsManager, + evaluateSslExceptionHandler(), + securityRestHandler, + SSLConfig + ) ); } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java index 3963e443d8..c28a1bdc1d 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java @@ -25,7 +25,7 @@ import org.opensearch.security.configuration.ConfigurationRepository; import org.opensearch.security.hasher.PasswordHasher; import org.opensearch.security.privileges.PrivilegesEvaluator; -import org.opensearch.security.ssl.SecurityKeyStore; +import org.opensearch.security.ssl.SslSettingsManager; import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.security.user.UserService; import org.opensearch.threadpool.ThreadPool; @@ -46,7 +46,7 @@ public static Collection getHandler( final PrivilegesEvaluator evaluator, final ThreadPool threadPool, final AuditLog auditLog, - final SecurityKeyStore securityKeyStore, + final SslSettingsManager sslSettingsManager, final UserService userService, final boolean certificatesReloadEnabled, final PasswordHasher passwordHasher @@ -97,7 +97,13 @@ public static Collection getHandler( new MultiTenancyConfigApiAction(clusterService, threadPool, securityApiDependencies), new RateLimitersApiAction(clusterService, threadPool, securityApiDependencies), new ConfigUpgradeApiAction(clusterService, threadPool, securityApiDependencies), - new SecuritySSLCertsApiAction(clusterService, threadPool, securityKeyStore, certificatesReloadEnabled, securityApiDependencies), + new SecuritySSLCertsApiAction( + clusterService, + threadPool, + sslSettingsManager, + certificatesReloadEnabled, + securityApiDependencies + ), new CertificatesApiAction(clusterService, threadPool, securityApiDependencies) ); } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsApiAction.java index 7f4bff50ab..5233149c66 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsApiAction.java @@ -12,11 +12,10 @@ package org.opensearch.security.dlic.rest.api; import java.io.IOException; -import java.security.cert.X509Certificate; -import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import java.util.stream.Stream; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -31,8 +30,10 @@ import org.opensearch.rest.RestRequest.Method; import org.opensearch.security.dlic.rest.validation.ValidationResult; import org.opensearch.security.securityconf.impl.CType; -import org.opensearch.security.ssl.SecurityKeyStore; -import org.opensearch.security.ssl.util.SSLConfigConstants; +import org.opensearch.security.ssl.SslContextHandler; +import org.opensearch.security.ssl.SslSettingsManager; +import org.opensearch.security.ssl.config.CertType; +import org.opensearch.security.ssl.config.Certificate; import org.opensearch.security.support.ConfigConstants; import org.opensearch.threadpool.ThreadPool; @@ -62,23 +63,20 @@ public class SecuritySSLCertsApiAction extends AbstractApiAction { ) ); - private final SecurityKeyStore securityKeyStore; + private final SslSettingsManager sslSettingsManager; private final boolean certificatesReloadEnabled; - private final boolean httpsEnabled; - public SecuritySSLCertsApiAction( final ClusterService clusterService, final ThreadPool threadPool, - final SecurityKeyStore securityKeyStore, + final SslSettingsManager sslSettingsManager, final boolean certificatesReloadEnabled, final SecurityApiDependencies securityApiDependencies ) { super(Endpoint.SSL, clusterService, threadPool, securityApiDependencies); - this.securityKeyStore = securityKeyStore; + this.sslSettingsManager = sslSettingsManager; this.certificatesReloadEnabled = certificatesReloadEnabled; - this.httpsEnabled = securityApiDependencies.settings().getAsBoolean(SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED, true); this.requestHandlersBuilder.configureRequestHandlers(this::securitySSLCertsRequestHandlers); } @@ -108,10 +106,10 @@ private void securitySSLCertsRequestHandlers(RequestHandler.RequestHandlersBuild .verifyAccessForAllMethods() .override( Method.GET, - (channel, request, client) -> withSecurityKeyStore().valid(keyStore -> loadCertificates(channel, keyStore)) + (channel, request, client) -> withSecurityKeyStore().valid(ignore -> loadCertificates(channel)) .error((status, toXContent) -> response(channel, status, toXContent)) ) - .override(Method.PUT, (channel, request, client) -> withSecurityKeyStore().valid(keyStore -> { + .override(Method.PUT, (channel, request, client) -> withSecurityKeyStore().valid(ignore -> { if (!certificatesReloadEnabled) { badRequest( channel, @@ -123,7 +121,7 @@ private void securitySSLCertsRequestHandlers(RequestHandler.RequestHandlersBuild ) ); } else { - reloadCertificates(channel, request, keyStore); + reloadCertificates(channel, request); } }).error((status, toXContent) -> response(channel, status, toXContent))); } @@ -138,65 +136,70 @@ boolean accessHandler(final RestRequest request) { } } - ValidationResult withSecurityKeyStore() { - if (securityKeyStore == null) { + ValidationResult withSecurityKeyStore() { + if (sslSettingsManager == null) { return ValidationResult.error(RestStatus.OK, badRequestMessage("keystore is not initialized")); } - return ValidationResult.success(securityKeyStore); + return ValidationResult.success(sslSettingsManager); } - protected void loadCertificates(final RestChannel channel, final SecurityKeyStore keyStore) throws IOException { + protected void loadCertificates(final RestChannel channel) throws IOException { ok( channel, (builder, params) -> builder.startObject() - .field("http_certificates_list", httpsEnabled ? generateCertDetailList(keyStore.getHttpCerts()) : null) - .field("transport_certificates_list", generateCertDetailList(keyStore.getTransportCerts())) + .field( + "http_certificates_list", + generateCertDetailList( + sslSettingsManager.sslContextHandler(CertType.HTTP).map(SslContextHandler::keyMaterialCertificates).orElse(null) + ) + ) + .field( + "transport_certificates_list", + generateCertDetailList( + sslSettingsManager.sslContextHandler(CertType.TRANSPORT) + .map(SslContextHandler::keyMaterialCertificates) + .orElse(null) + ) + ) .endObject() ); } - private List> generateCertDetailList(final X509Certificate[] certs) { + private List> generateCertDetailList(final Stream certs) { if (certs == null) { return null; } - return Arrays.stream(certs).map(cert -> { - final String issuerDn = cert != null && cert.getIssuerX500Principal() != null ? cert.getIssuerX500Principal().getName() : ""; - final String subjectDn = cert != null && cert.getSubjectX500Principal() != null ? cert.getSubjectX500Principal().getName() : ""; - - final String san = securityKeyStore.getSubjectAlternativeNames(cert); - - final String notBefore = cert != null && cert.getNotBefore() != null ? cert.getNotBefore().toInstant().toString() : ""; - final String notAfter = cert != null && cert.getNotAfter() != null ? cert.getNotAfter().toInstant().toString() : ""; - return ImmutableMap.of( + return certs.map( + c -> ImmutableMap.of( "issuer_dn", - issuerDn, + c.issuer(), "subject_dn", - subjectDn, + c.subject(), "san", - san, + c.subjectAlternativeNames(), "not_before", - notBefore, + c.notBefore(), "not_after", - notAfter - ); - }).collect(Collectors.toList()); + c.notAfter() + ) + ).collect(Collectors.toList()); } - protected void reloadCertificates(final RestChannel channel, final RestRequest request, final SecurityKeyStore keyStore) - throws IOException { + protected void reloadCertificates(final RestChannel channel, final RestRequest request) throws IOException { final String certType = request.param("certType").toLowerCase().trim(); try { switch (certType) { case "http": - if (!httpsEnabled) { + if (sslSettingsManager.sslConfiguration(CertType.HTTP).isPresent()) { + sslSettingsManager.reloadSslContext(CertType.HTTP); + ok(channel, (builder, params) -> builder.startObject().field("message", "updated http certs").endObject()); + } else { badRequest(channel, "SSL for HTTP is disabled"); - return; } - keyStore.initHttpSSLConfig(); - ok(channel, (builder, params) -> builder.startObject().field("message", "updated http certs").endObject()); break; case "transport": - keyStore.initTransportSSLConfig(); + sslSettingsManager.reloadSslContext(CertType.TRANSPORT); + sslSettingsManager.reloadSslContext(CertType.TRANSPORT_CLIENT); ok(channel, (builder, params) -> builder.startObject().field("message", "updated transport certs").endObject()); break; default: diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/ssl/TransportCertificatesInfoNodesAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/ssl/TransportCertificatesInfoNodesAction.java index 681c2c01eb..39edfd570f 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/ssl/TransportCertificatesInfoNodesAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/ssl/TransportCertificatesInfoNodesAction.java @@ -12,22 +12,22 @@ package org.opensearch.security.dlic.rest.api.ssl; import java.io.IOException; -import java.security.cert.X509Certificate; import java.util.List; import java.util.Map; - -import com.google.common.collect.ImmutableList; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.opensearch.action.FailedNodeException; import org.opensearch.action.support.ActionFilters; import org.opensearch.action.support.nodes.TransportNodesAction; import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.inject.Inject; -import org.opensearch.common.settings.Settings; import org.opensearch.core.common.io.stream.StreamInput; import org.opensearch.core.common.io.stream.StreamOutput; -import org.opensearch.security.ssl.DefaultSecurityKeyStore; -import org.opensearch.security.ssl.util.SSLConfigConstants; +import org.opensearch.security.ssl.SslContextHandler; +import org.opensearch.security.ssl.SslSettingsManager; +import org.opensearch.security.ssl.config.CertType; +import org.opensearch.security.ssl.config.Certificate; import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.TransportRequest; import org.opensearch.transport.TransportService; @@ -38,18 +38,15 @@ public class TransportCertificatesInfoNodesAction extends TransportNodesAction< TransportCertificatesInfoNodesAction.NodeRequest, CertificatesNodesResponse.CertificatesNodeResponse> { - private final DefaultSecurityKeyStore securityKeyStore; - - private final boolean httpsEnabled; + private final SslSettingsManager sslSettingsManager; @Inject public TransportCertificatesInfoNodesAction( - final Settings settings, final ThreadPool threadPool, final ClusterService clusterService, final TransportService transportService, final ActionFilters actionFilters, - final DefaultSecurityKeyStore securityKeyStore + final SslSettingsManager sslSettingsManager ) { super( CertificatesActionType.NAME, @@ -62,8 +59,7 @@ public TransportCertificatesInfoNodesAction( ThreadPool.Names.GENERIC, CertificatesNodesResponse.CertificatesNodeResponse.class ); - this.httpsEnabled = settings.getAsBoolean(SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED, true); - this.securityKeyStore = securityKeyStore; + this.sslSettingsManager = sslSettingsManager; } @Override @@ -89,12 +85,6 @@ protected CertificatesNodesResponse.CertificatesNodeResponse newNodeResponse(fin protected CertificatesNodesResponse.CertificatesNodeResponse nodeOperation(final NodeRequest request) { final var sslCertRequest = request.sslCertsInfoNodesRequest; - if (securityKeyStore == null) { - return new CertificatesNodesResponse.CertificatesNodeResponse( - clusterService.localNode(), - new IllegalStateException("keystore is not initialized") - ); - } try { return new CertificatesNodesResponse.CertificatesNodeResponse( clusterService.localNode(), @@ -109,23 +99,27 @@ protected CertificatesInfo loadCertificates(final CertificateType certificateTyp var httpCertificates = List.of(); var transportsCertificates = List.of(); if (CertificateType.isHttp(certificateType)) { - httpCertificates = httpsEnabled ? certificatesDetails(securityKeyStore.getHttpCerts()) : List.of(); + httpCertificates = sslSettingsManager.sslContextHandler(CertType.HTTP) + .map(SslContextHandler::keyMaterialCertificates) + .map(this::certificatesDetails) + .orElse(List.of()); } if (CertificateType.isTransport(certificateType)) { - transportsCertificates = certificatesDetails(securityKeyStore.getTransportCerts()); + transportsCertificates = sslSettingsManager.sslContextHandler(CertType.TRANSPORT) + .map(SslContextHandler::keyMaterialCertificates) + .map(this::certificatesDetails) + .orElse(List.of()); } return new CertificatesInfo(Map.of(CertificateType.HTTP, httpCertificates, CertificateType.TRANSPORT, transportsCertificates)); } - private List certificatesDetails(final X509Certificate[] certs) { - if (certs == null) { + private List certificatesDetails(final Stream certificateStream) { + if (certificateStream == null) { return null; } - final var certificates = ImmutableList.builder(); - for (final var c : certs) { - certificates.add(CertificateInfo.from(c, securityKeyStore.getSubjectAlternativeNames(c))); - } - return certificates.build(); + return certificateStream.map( + c -> new CertificateInfo(c.subject(), c.subjectAlternativeNames(), c.issuer(), c.notAfter(), c.notBefore()) + ).collect(Collectors.toList()); } public static class NodeRequest extends TransportRequest { diff --git a/src/main/java/org/opensearch/security/ssl/OpenSearchSecureSettingsFactory.java b/src/main/java/org/opensearch/security/ssl/OpenSearchSecureSettingsFactory.java index 9d482b18a8..43f6cc4f29 100644 --- a/src/main/java/org/opensearch/security/ssl/OpenSearchSecureSettingsFactory.java +++ b/src/main/java/org/opensearch/security/ssl/OpenSearchSecureSettingsFactory.java @@ -25,6 +25,7 @@ import org.opensearch.plugins.SecureTransportSettingsProvider; import org.opensearch.plugins.TransportExceptionHandler; import org.opensearch.security.filter.SecurityRestFilter; +import org.opensearch.security.ssl.config.CertType; import org.opensearch.security.ssl.http.netty.Netty4ConditionalDecompressor; import org.opensearch.security.ssl.http.netty.Netty4HttpRequestHeaderVerifier; import org.opensearch.security.ssl.transport.SSLConfig; @@ -36,20 +37,20 @@ public class OpenSearchSecureSettingsFactory implements SecureSettingsFactory { private final ThreadPool threadPool; - private final SecurityKeyStore sks; + private final SslSettingsManager sslSettingsManager; private final SslExceptionHandler sslExceptionHandler; private final SecurityRestFilter restFilter; private final SSLConfig sslConfig; public OpenSearchSecureSettingsFactory( ThreadPool threadPool, - SecurityKeyStore sks, + SslSettingsManager sslSettingsManager, SslExceptionHandler sslExceptionHandler, SecurityRestFilter restFilter, SSLConfig sslConfig ) { this.threadPool = threadPool; - this.sks = sks; + this.sslSettingsManager = sslSettingsManager; this.sslExceptionHandler = sslExceptionHandler; this.restFilter = restFilter; this.sslConfig = sslConfig; @@ -80,12 +81,12 @@ public boolean dualModeEnabled() { @Override public Optional buildSecureServerTransportEngine(Settings settings, Transport transport) throws SSLException { - return Optional.of(sks.createServerTransportSSLEngine()); + return sslSettingsManager.sslContextHandler(CertType.TRANSPORT).map(SslContextHandler::createSSLEngine); } @Override public Optional buildSecureClientTransportEngine(Settings settings, String hostname, int port) throws SSLException { - return Optional.of(sks.createClientTransportSSLEngine(hostname, port)); + return sslSettingsManager.sslContextHandler(CertType.TRANSPORT_CLIENT).map(c -> c.createSSLEngine(hostname, port)); } }); } @@ -142,7 +143,7 @@ public void onError(Throwable t) { @Override public Optional buildSecureHttpServerEngine(Settings settings, HttpServerTransport transport) throws SSLException { - return Optional.of(sks.createHTTPSSLEngine()); + return sslSettingsManager.sslContextHandler(CertType.HTTP).map(SslContextHandler::createSSLEngine); } }); } diff --git a/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java b/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java index c16706c870..25c55f3cbb 100644 --- a/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java +++ b/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java @@ -126,7 +126,7 @@ public class OpenSearchSecuritySSLPlugin extends Plugin implements SystemIndexPl protected final Settings settings; protected volatile SecurityRestFilter securityRestHandler; protected final SharedGroupFactory sharedGroupFactory; - protected final SecurityKeyStore sks; + protected final SslSettingsManager sslSettingsManager; protected PrincipalExtractor principalExtractor; protected final Path configPath; private final static SslExceptionHandler NOOP_SSL_EXCEPTION_HANDLER = new SslExceptionHandler() { @@ -144,7 +144,7 @@ protected OpenSearchSecuritySSLPlugin(final Settings settings, final Path config this.httpSSLEnabled = false; this.transportSSLEnabled = false; this.extendedKeyUsageEnabled = false; - this.sks = null; + this.sslSettingsManager = null; this.configPath = null; SSLConfig = new SSLConfig(false, false); @@ -246,11 +246,7 @@ public Object run() { log.error("SSL not activated for http and/or transport."); } - if (ExternalSecurityKeyStore.hasExternalSslContext(settings)) { - this.sks = new ExternalSecurityKeyStore(settings); - } else { - this.sks = new DefaultSecurityKeyStore(settings, configPath); - } + this.sslSettingsManager = new SslSettingsManager(new Environment(settings, configPath)); } @Override @@ -311,7 +307,7 @@ public List getRestHandlers( final List handlers = new ArrayList(1); if (!client) { - handlers.add(new SecuritySSLInfoAction(settings, configPath, restController, sks, Objects.requireNonNull(principalExtractor))); + handlers.add(new SecuritySSLInfoAction(settings, configPath, sslSettingsManager, Objects.requireNonNull(principalExtractor))); } return handlers; @@ -675,7 +671,7 @@ public List getSettingsFilter() { @Override public Optional getSecureSettingFactory(Settings settings) { return Optional.of( - new OpenSearchSecureSettingsFactory(threadPool, sks, NOOP_SSL_EXCEPTION_HANDLER, securityRestHandler, SSLConfig) + new OpenSearchSecureSettingsFactory(threadPool, sslSettingsManager, NOOP_SSL_EXCEPTION_HANDLER, securityRestHandler, SSLConfig) ); } diff --git a/src/main/java/org/opensearch/security/ssl/SecureSSLSettings.java b/src/main/java/org/opensearch/security/ssl/SecureSSLSettings.java index 171bb18bb5..5aad07fbdd 100644 --- a/src/main/java/org/opensearch/security/ssl/SecureSSLSettings.java +++ b/src/main/java/org/opensearch/security/ssl/SecureSSLSettings.java @@ -36,7 +36,7 @@ public final class SecureSSLSettings { private static final Logger LOG = LogManager.getLogger(SecureSSLSettings.class); - private static final String SECURE_SUFFIX = "_secure"; + public static final String SECURE_SUFFIX = "_secure"; private static final String PREFIX = "plugins.security.ssl"; private static final String HTTP_PREFIX = PREFIX + ".http"; private static final String TRANSPORT_PREFIX = PREFIX + ".transport"; diff --git a/src/main/java/org/opensearch/security/ssl/SslConfiguration.java b/src/main/java/org/opensearch/security/ssl/SslConfiguration.java new file mode 100644 index 0000000000..2332867bd8 --- /dev/null +++ b/src/main/java/org/opensearch/security/ssl/SslConfiguration.java @@ -0,0 +1,148 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.ssl; + +import java.nio.file.Path; +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.OpenSearchException; +import org.opensearch.security.ssl.config.Certificate; +import org.opensearch.security.ssl.config.KeyStoreConfiguration; +import org.opensearch.security.ssl.config.SslParameters; +import org.opensearch.security.ssl.config.TrustStoreConfiguration; + +import io.netty.handler.codec.http2.Http2SecurityUtil; +import io.netty.handler.ssl.ApplicationProtocolConfig; +import io.netty.handler.ssl.ApplicationProtocolNames; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.SupportedCipherSuiteFilter; + +public class SslConfiguration { + + private final static Logger LOGGER = LogManager.getLogger(SslConfiguration.class); + + private final SslParameters sslParameters; + + private final TrustStoreConfiguration trustStoreConfiguration; + + private final KeyStoreConfiguration keyStoreConfiguration; + + public SslConfiguration( + final SslParameters sslParameters, + final TrustStoreConfiguration trustStoreConfiguration, + final KeyStoreConfiguration keyStoreConfiguration + ) { + this.sslParameters = sslParameters; + this.trustStoreConfiguration = trustStoreConfiguration; + this.keyStoreConfiguration = keyStoreConfiguration; + } + + public List dependentFiles() { + return Stream.concat(keyStoreConfiguration.files().stream(), Stream.of(trustStoreConfiguration.file())) + .collect(Collectors.toList()); + } + + public List certificates() { + return Stream.concat(trustStoreConfiguration.loadCertificates().stream(), keyStoreConfiguration.loadCertificates().stream()) + .collect(Collectors.toList()); + } + + public SslParameters sslParameters() { + return sslParameters; + } + + @SuppressWarnings("removal") + SslContext buildServerSslContext(final boolean validateCertificates) { + try { + return AccessController.doPrivileged( + (PrivilegedExceptionAction) () -> SslContextBuilder.forServer( + keyStoreConfiguration.createKeyManagerFactory(validateCertificates) + ) + .sslProvider(sslParameters.provider()) + .clientAuth(sslParameters.clientAuth()) + .protocols(sslParameters.allowedProtocols().toArray(new String[0])) + // TODO we always add all HTTP 2 ciphers, while maybe it is better to set them differently + .ciphers( + Stream.concat( + Http2SecurityUtil.CIPHERS.stream(), + StreamSupport.stream(sslParameters.allowedCiphers().spliterator(), false) + ).collect(Collectors.toSet()), + SupportedCipherSuiteFilter.INSTANCE + ) + .sessionCacheSize(0) + .sessionTimeout(0) + .applicationProtocolConfig( + new ApplicationProtocolConfig( + ApplicationProtocolConfig.Protocol.ALPN, + // NO_ADVERTISE is currently the only mode supported by both OpenSsl and JDK providers. + ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE, + // ACCEPT is currently the only mode supported by both OpenSsl and JDK providers. + ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT, + ApplicationProtocolNames.HTTP_2, + ApplicationProtocolNames.HTTP_1_1 + ) + ) + .trustManager(trustStoreConfiguration.createTrustManagerFactory(validateCertificates)) + .build() + ); + } catch (PrivilegedActionException e) { + throw new OpenSearchException("Filed to build server SSL context", e); + } + } + + @SuppressWarnings("removal") + SslContext buildClientSslContext(final boolean validateCertificates) { + try { + return AccessController.doPrivileged( + (PrivilegedExceptionAction) () -> SslContextBuilder.forClient() + .sslProvider(sslParameters.provider()) + .protocols(sslParameters.allowedProtocols()) + .ciphers(sslParameters.allowedCiphers()) + .applicationProtocolConfig(ApplicationProtocolConfig.DISABLED) + .sessionCacheSize(0) + .sessionTimeout(0) + .sslProvider(sslParameters.provider()) + .keyManager(keyStoreConfiguration.createKeyManagerFactory(validateCertificates)) + .trustManager(trustStoreConfiguration.createTrustManagerFactory(validateCertificates)) + .build() + ); + } catch (PrivilegedActionException e) { + throw new OpenSearchException("Filed to build client SSL context", e); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SslConfiguration that = (SslConfiguration) o; + return Objects.equals(sslParameters, that.sslParameters) + && Objects.equals(trustStoreConfiguration, that.trustStoreConfiguration) + && Objects.equals(keyStoreConfiguration, that.keyStoreConfiguration); + } + + @Override + public int hashCode() { + return Objects.hash(sslParameters, trustStoreConfiguration, keyStoreConfiguration); + } +} diff --git a/src/main/java/org/opensearch/security/ssl/SslContextHandler.java b/src/main/java/org/opensearch/security/ssl/SslContextHandler.java new file mode 100644 index 0000000000..9fda1641af --- /dev/null +++ b/src/main/java/org/opensearch/security/ssl/SslContextHandler.java @@ -0,0 +1,165 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.ssl; + +import java.nio.charset.StandardCharsets; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.net.ssl.SSLEngine; + +import org.opensearch.security.ssl.config.Certificate; +import org.opensearch.transport.NettyAllocator; + +import io.netty.handler.ssl.SslContext; + +public class SslContextHandler { + + private SslContext sslContext; + + private final SslConfiguration sslConfiguration; + + private final List loadedCertificates; + + public SslContextHandler(final SslConfiguration sslConfiguration) { + this(sslConfiguration, false); + } + + public SslContextHandler(final SslConfiguration sslConfiguration, final boolean client) { + this.sslContext = client ? sslConfiguration.buildClientSslContext(true) : sslConfiguration.buildServerSslContext(true); + this.sslConfiguration = sslConfiguration; + this.loadedCertificates = sslConfiguration.certificates(); + } + + public SSLEngine createSSLEngine() { + return sslContext.newEngine(NettyAllocator.getAllocator()); + } + + public SSLEngine createSSLEngine(final String hostname, final int port) { + return sslContext.newEngine(NettyAllocator.getAllocator(), hostname, port); + } + + public SslConfiguration sslConfiguration() { + return sslConfiguration; + } + + SslContext sslContext() { + return sslContext; + } + + public Stream keyMaterialCertificates() { + return keyMaterialCertificates(loadedCertificates); + } + + Stream keyMaterialCertificates(final List certificates) { + return certificates.stream().filter(Certificate::hasKey); + } + + void reloadSslContext() throws CertificateException { + final var newCertificates = sslConfiguration.certificates(); + + if (sameCertificates(newCertificates)) { + return; + } + validateNewCertificates(newCertificates); + invalidateSessions(); + if (sslContext.isClient()) { + sslContext = sslConfiguration.buildClientSslContext(false); + } else { + sslContext = sslConfiguration.buildServerSslContext(false); + } + loadedCertificates.clear(); + loadedCertificates.addAll(newCertificates); + } + + private boolean sameCertificates(final List newCertificates) { + final Set currentCertSignatureSet = keyMaterialCertificates().map(Certificate::x509Certificate) + .map(X509Certificate::getSignature) + .map(s -> new String(s, StandardCharsets.UTF_8)) + .collect(Collectors.toSet()); + final Set newCertSignatureSet = keyMaterialCertificates(newCertificates).map(Certificate::x509Certificate) + .map(X509Certificate::getSignature) + .map(s -> new String(s, StandardCharsets.UTF_8)) + .collect(Collectors.toSet()); + return currentCertSignatureSet.equals(newCertSignatureSet); + } + + private void validateSubjectDns(final List newCertificates) throws CertificateException { + final List currentSubjectDNs = keyMaterialCertificates().map(Certificate::subject).sorted().collect(Collectors.toList()); + final List newSubjectDNs = keyMaterialCertificates(newCertificates).map(Certificate::subject) + .sorted() + .collect(Collectors.toList()); + if (!currentSubjectDNs.equals(newSubjectDNs)) { + throw new CertificateException( + "New certificates do not have valid Subject DNs. Current Subject DNs " + + currentSubjectDNs + + " new Subject DNs " + + newSubjectDNs + ); + } + } + + private void validateIssuerDns(final List newCertificates) throws CertificateException { + final List currentIssuerDNs = keyMaterialCertificates().map(Certificate::issuer).sorted().collect(Collectors.toList()); + final List newIssuerDNs = keyMaterialCertificates(newCertificates).map(Certificate::issuer) + .sorted() + .collect(Collectors.toList()); + if (!currentIssuerDNs.equals(newIssuerDNs)) { + throw new CertificateException( + "New certificates do not have valid Issuer DNs. Current Issuer DNs: " + + currentIssuerDNs + + " new Issuer DNs: " + + newIssuerDNs + ); + } + } + + private void validateSans(final List newCertificates) throws CertificateException { + final List currentSans = keyMaterialCertificates().map(Certificate::subjectAlternativeNames) + .sorted() + .collect(Collectors.toList()); + final List newSans = keyMaterialCertificates(newCertificates).map(Certificate::subjectAlternativeNames) + .sorted() + .collect(Collectors.toList()); + if (!currentSans.equals(newSans)) { + throw new CertificateException( + "New certificates do not have valid SANs. Current SANs: " + currentSans + " new SANs: " + newSans + ); + } + } + + private void validateNewCertificates(final List newCertificates) throws CertificateException { + for (final var certificate : newCertificates) { + certificate.x509Certificate().checkValidity(); + } + validateSubjectDns(newCertificates); + validateIssuerDns(newCertificates); + validateSans(newCertificates); + } + + private void invalidateSessions() { + final var sessionContext = sslContext.sessionContext(); + if (sessionContext != null) { + for (final var sessionId : Collections.list(sessionContext.getIds())) { + final var session = sessionContext.getSession(sessionId); + if (session != null) { + session.invalidate(); + } + } + } + } + +} diff --git a/src/main/java/org/opensearch/security/ssl/SslSettingsManager.java b/src/main/java/org/opensearch/security/ssl/SslSettingsManager.java new file mode 100644 index 0000000000..381c510894 --- /dev/null +++ b/src/main/java/org/opensearch/security/ssl/SslSettingsManager.java @@ -0,0 +1,384 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.ssl; + +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import javax.crypto.Cipher; + +import com.google.common.collect.ImmutableMap; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.OpenSearchException; +import org.opensearch.common.settings.Settings; +import org.opensearch.env.Environment; +import org.opensearch.security.ssl.config.CertType; +import org.opensearch.security.ssl.config.SslCertificatesLoader; +import org.opensearch.security.ssl.config.SslParameters; + +import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.OpenSsl; +import io.netty.util.internal.PlatformDependent; + +import static org.opensearch.security.ssl.util.SSLConfigConstants.CLIENT_AUTH_MODE; +import static org.opensearch.security.ssl.util.SSLConfigConstants.ENABLED; +import static org.opensearch.security.ssl.util.SSLConfigConstants.EXTENDED_KEY_USAGE_ENABLED; +import static org.opensearch.security.ssl.util.SSLConfigConstants.KEYSTORE_ALIAS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.KEYSTORE_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.PEM_CERT_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.PEM_KEY_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.PEM_TRUSTED_CAS_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED_DEFAULT; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_ENABLE_OPENSSL_IF_AVAILABLE; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_KEYSTORE_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_PEMCERT_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_PEMKEY_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_PEMTRUSTEDCAS_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_TRUSTSTORE_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_CLIENT_KEYSTORE_ALIAS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_CLIENT_PEMCERT_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_CLIENT_PEMKEY_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_CLIENT_PEMTRUSTEDCAS_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_CLIENT_TRUSTSTORE_ALIAS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENABLED_DEFAULT; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENABLE_OPENSSL_IF_AVAILABLE; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED_DEFAULT; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_PEMCERT_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_PEMKEY_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_PEMTRUSTEDCAS_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_SERVER_KEYSTORE_ALIAS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_SERVER_PEMCERT_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_SERVER_PEMKEY_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_SERVER_PEMTRUSTEDCAS_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_SERVER_TRUSTSTORE_ALIAS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_TRANSPORT_CLIENT_EXTENDED_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_TRANSPORT_SERVER_EXTENDED_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.TRUSTSTORE_ALIAS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.TRUSTSTORE_FILEPATH; + +public class SslSettingsManager { + + private final static Logger LOGGER = LogManager.getLogger(SslSettingsManager.class); + + private final Map sslSettingsContexts; + + public SslSettingsManager(final Environment environment) { + this.sslSettingsContexts = buildSslContexts(environment); + } + + public Optional sslConfiguration(final CertType certType) { + return Optional.ofNullable(sslSettingsContexts.get(certType)).map(SslContextHandler::sslConfiguration); + } + + public Optional sslContextHandler(final CertType sslConfigPrefix) { + return Optional.ofNullable(sslSettingsContexts.get(sslConfigPrefix)); + } + + private Map buildSslContexts(final Environment environment) { + final var contexts = new ImmutableMap.Builder(); + final var configurations = loadConfigurations(environment); + Optional.ofNullable(configurations.get(CertType.HTTP)) + .ifPresentOrElse( + sslConfiguration -> contexts.put(CertType.HTTP, new SslContextHandler(sslConfiguration)), + () -> LOGGER.warn("SSL Configuration for HTTP Layer hasn't been set") + ); + Optional.ofNullable(configurations.get(CertType.TRANSPORT)).ifPresentOrElse(sslConfiguration -> { + contexts.put(CertType.TRANSPORT, new SslContextHandler(sslConfiguration)); + final var transportClientConfiguration = Optional.ofNullable(configurations.get(CertType.TRANSPORT_CLIENT)) + .orElse(sslConfiguration); + contexts.put(CertType.TRANSPORT_CLIENT, new SslContextHandler(transportClientConfiguration, true)); + }, () -> LOGGER.warn("SSL Configuration for Transport Layer hasn't been set")); + return contexts.build(); + } + + public synchronized void reloadSslContext(final CertType certType) { + sslContextHandler(certType).ifPresentOrElse(sscContextHandler -> { + LOGGER.info("Reloading {} SSL context", certType.name()); + try { + sscContextHandler.reloadSslContext(); + } catch (CertificateException e) { + throw new OpenSearchException(e); + } + LOGGER.info("{} SSL context reloaded", certType.name()); + }, () -> LOGGER.error("Missing SSL Context for {}", certType.name())); + } + + private Map loadConfigurations(final Environment environment) { + final var settings = environment.settings(); + final var httpSettings = settings.getByPrefix(CertType.HTTP.sslConfigPrefix()); + final var transpotSettings = settings.getByPrefix(CertType.TRANSPORT.sslConfigPrefix()); + if (httpSettings.isEmpty() && transpotSettings.isEmpty()) { + throw new OpenSearchException("No SSL configuration found"); + } + jceWarnings(); + openSslWarnings(settings); + + final var httpEnabled = httpSettings.getAsBoolean(ENABLED, SECURITY_SSL_HTTP_ENABLED_DEFAULT); + final var transportEnabled = transpotSettings.getAsBoolean(ENABLED, SECURITY_SSL_TRANSPORT_ENABLED_DEFAULT); + + final var configurationBuilder = ImmutableMap.builder(); + if (httpEnabled && !clientNode(settings)) { + validateHttpSettings(httpSettings); + final var httpSslParameters = SslParameters.loader(httpSettings).load(true); + final var httpTrustAndKeyStore = new SslCertificatesLoader(CertType.HTTP.sslConfigPrefix()).loadConfiguration(environment); + configurationBuilder.put( + CertType.HTTP, + new SslConfiguration(httpSslParameters, httpTrustAndKeyStore.v1(), httpTrustAndKeyStore.v2()) + ); + LOGGER.info("TLS HTTP Provider : {}", httpSslParameters.provider()); + LOGGER.info("Enabled TLS protocols for HTTP layer : {}", httpSslParameters.allowedProtocols()); + } + final var transportSslParameters = SslParameters.loader(transpotSettings).load(false); + if (transportEnabled) { + if (hasExtendedKeyUsageEnabled(transpotSettings)) { + validateTransportSettings(transpotSettings); + final var transportServerTrustAndKeyStore = new SslCertificatesLoader( + CertType.TRANSPORT.sslConfigPrefix(), + SSL_TRANSPORT_SERVER_EXTENDED_PREFIX + ).loadConfiguration(environment); + configurationBuilder.put( + CertType.TRANSPORT, + new SslConfiguration(transportSslParameters, transportServerTrustAndKeyStore.v1(), transportServerTrustAndKeyStore.v2()) + ); + final var transportClientTrustAndKeyStore = new SslCertificatesLoader( + CertType.TRANSPORT.sslConfigPrefix(), + SSL_TRANSPORT_CLIENT_EXTENDED_PREFIX + ).loadConfiguration(environment); + configurationBuilder.put( + CertType.TRANSPORT_CLIENT, + new SslConfiguration(transportSslParameters, transportClientTrustAndKeyStore.v1(), transportClientTrustAndKeyStore.v2()) + ); + } else { + validateTransportSettings(transpotSettings); + final var transportTrustAndKeyStore = new SslCertificatesLoader(CertType.TRANSPORT.sslConfigPrefix()).loadConfiguration( + environment + ); + configurationBuilder.put( + CertType.TRANSPORT, + new SslConfiguration(transportSslParameters, transportTrustAndKeyStore.v1(), transportTrustAndKeyStore.v2()) + ); + } + LOGGER.info("TLS Transport Client Provider : {}", transportSslParameters.provider()); + LOGGER.info("TLS Transport Server Provider : {}", transportSslParameters.provider()); + LOGGER.info("Enabled TLS protocols for Transport layer : {}", transportSslParameters.allowedProtocols()); + } + return configurationBuilder.build(); + } + + private boolean clientNode(final Settings settings) { + return !"node".equals(settings.get(OpenSearchSecuritySSLPlugin.CLIENT_TYPE)); + } + + private void validateHttpSettings(final Settings httpSettings) { + if (httpSettings == null) return; + if (!httpSettings.getAsBoolean(ENABLED, SECURITY_SSL_HTTP_ENABLED_DEFAULT)) return; + + final var clientAuth = ClientAuth.valueOf(httpSettings.get(CLIENT_AUTH_MODE, ClientAuth.OPTIONAL.name()).toUpperCase(Locale.ROOT)); + + if (hasPemStoreSettings(httpSettings)) { + if (!httpSettings.hasValue(PEM_CERT_FILEPATH) || !httpSettings.hasValue(PEM_KEY_FILEPATH)) { + throw new OpenSearchException( + "Wrong HTTP SSL configuration. " + + String.join(", ", SECURITY_SSL_HTTP_PEMCERT_FILEPATH, SECURITY_SSL_HTTP_PEMKEY_FILEPATH) + + " must be set" + ); + } + if (clientAuth == ClientAuth.REQUIRE && !httpSettings.hasValue(PEM_TRUSTED_CAS_FILEPATH)) { + throw new OpenSearchException( + "Wrong HTTP SSL configuration. " + SECURITY_SSL_HTTP_PEMTRUSTEDCAS_FILEPATH + " must be set if client auth is required" + ); + } + } else if (hasKeyOrTrustStoreSettings(httpSettings)) { + if (!httpSettings.hasValue(KEYSTORE_FILEPATH)) { + throw new OpenSearchException("Wrong HTTP SSL configuration. " + SECURITY_SSL_HTTP_KEYSTORE_FILEPATH + " must be set"); + } + if (clientAuth == ClientAuth.REQUIRE && !httpSettings.hasValue(TRUSTSTORE_FILEPATH)) { + throw new OpenSearchException( + "Wrong HTTP SSL configuration. " + SECURITY_SSL_HTTP_TRUSTSTORE_FILEPATH + " must be set if client auth is required" + ); + } + } else { + throw new OpenSearchException( + "Wrong HTTP SSL configuration. One of Keystore and Truststore files or X.509 PEM certificates and " + + "PKCS#8 keys groups should be set to configure HTTP layer" + ); + } + } + + private void validateTransportSettings(final Settings transportSettings) { + if (!hasExtendedKeyUsageEnabled(transportSettings)) { + if (hasPemStoreSettings(transportSettings)) { + if (!transportSettings.hasValue(PEM_CERT_FILEPATH) + || !transportSettings.hasValue(PEM_KEY_FILEPATH) + || !transportSettings.hasValue(PEM_TRUSTED_CAS_FILEPATH)) { + throw new OpenSearchException( + "Wrong Transport SSL configuration. " + + String.join( + ",", + SECURITY_SSL_TRANSPORT_PEMCERT_FILEPATH, + SECURITY_SSL_TRANSPORT_PEMKEY_FILEPATH, + SECURITY_SSL_TRANSPORT_PEMTRUSTEDCAS_FILEPATH + ) + + " must be set" + ); + } + + } else if (hasKeyOrTrustStoreSettings(transportSettings)) { + verifyKeyAndTrustStoreSettings(transportSettings); + } else { + throw new OpenSearchException( + "Wrong Transport SSL configuration. One of Keystore and Truststore files or X.509 PEM certificates and " + + "PKCS#8 keys groups should be set to configure Transport layer properly" + ); + } + } else { + final var serverTransportSettings = transportSettings.getByPrefix(SSL_TRANSPORT_SERVER_EXTENDED_PREFIX); + final var clientTransportSettings = transportSettings.getByPrefix(SSL_TRANSPORT_CLIENT_EXTENDED_PREFIX); + if (hasKeyOrTrustStoreSettings(transportSettings)) { + verifyKeyAndTrustStoreSettings(transportSettings); + if (!serverTransportSettings.hasValue(KEYSTORE_ALIAS) + || !serverTransportSettings.hasValue(TRUSTSTORE_ALIAS) + || !clientTransportSettings.hasValue(KEYSTORE_ALIAS) + || !clientTransportSettings.hasValue(TRUSTSTORE_ALIAS)) { + throw new OpenSearchException( + "Wrong Transport/Transport Client SSL configuration. " + + String.join( + ",", + SECURITY_SSL_TRANSPORT_SERVER_KEYSTORE_ALIAS, + SECURITY_SSL_TRANSPORT_SERVER_TRUSTSTORE_ALIAS, + SECURITY_SSL_TRANSPORT_CLIENT_KEYSTORE_ALIAS, + SECURITY_SSL_TRANSPORT_CLIENT_TRUSTSTORE_ALIAS + ) + + " must be set if " + + SECURITY_SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED + + " is set" + ); + } + } else if (!hasKeyOrTrustStoreSettings(transportSettings)) { + if (!serverTransportSettings.hasValue(PEM_CERT_FILEPATH) + || !serverTransportSettings.hasValue(PEM_KEY_FILEPATH) + || !serverTransportSettings.hasValue(PEM_TRUSTED_CAS_FILEPATH) + || !clientTransportSettings.hasValue(PEM_CERT_FILEPATH) + || !clientTransportSettings.hasValue(PEM_KEY_FILEPATH) + || !clientTransportSettings.hasValue(PEM_TRUSTED_CAS_FILEPATH)) { + throw new OpenSearchException( + "Wrong Transport/Transport Client SSL configuration. " + + String.join( + ",", + SECURITY_SSL_TRANSPORT_SERVER_PEMCERT_FILEPATH, + SECURITY_SSL_TRANSPORT_SERVER_PEMKEY_FILEPATH, + SECURITY_SSL_TRANSPORT_SERVER_PEMTRUSTEDCAS_FILEPATH, + SECURITY_SSL_TRANSPORT_CLIENT_PEMCERT_FILEPATH, + SECURITY_SSL_TRANSPORT_CLIENT_PEMKEY_FILEPATH, + SECURITY_SSL_TRANSPORT_CLIENT_PEMTRUSTEDCAS_FILEPATH + ) + + " must be set if " + + SECURITY_SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED + + " is set" + ); + } + } else { + throw new OpenSearchException( + "Wrong Transport/Transport Client SSL configuration. One of Keystore and Truststore files or X.509 PEM certificates and " + + "PKCS#8 keys groups should be set to configure HTTP layer" + ); + } + } + } + + private void verifyKeyAndTrustStoreSettings(final Settings settings) { + if (!settings.hasValue(KEYSTORE_FILEPATH) || !settings.hasValue(TRUSTSTORE_FILEPATH)) { + throw new OpenSearchException( + "Wrong Transport/Tran SSL configuration. One of Keystore and Truststore files or X.509 PEM certificates and " + + "PKCS#8 keys groups should be set to configure Transport layer properly" + ); + } + } + + private boolean hasExtendedKeyUsageEnabled(final Settings settings) { + return settings.getAsBoolean(EXTENDED_KEY_USAGE_ENABLED, SECURITY_SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED_DEFAULT); + } + + private boolean hasKeyOrTrustStoreSettings(final Settings settings) { + return settings.hasValue(KEYSTORE_FILEPATH) || settings.hasValue(TRUSTSTORE_FILEPATH); + } + + private boolean hasPemStoreSettings(final Settings settings) { + return settings.hasValue(PEM_KEY_FILEPATH) || settings.hasValue(PEM_CERT_FILEPATH) || settings.hasValue(PEM_TRUSTED_CAS_FILEPATH); + } + + void jceWarnings() { + try { + final int aesMaxKeyLength = Cipher.getMaxAllowedKeyLength("AES"); + + if (aesMaxKeyLength < 256) { + // CS-SUPPRESS-SINGLE: RegexpSingleline Java Cryptography Extension is unrelated to OpenSearch extensions + LOGGER.info( + "AES-256 not supported, max key length for AES is {} bit." + + " (This is not an issue, it just limits possible encryption strength. " + + "To enable AES 256, " + + "install 'Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files')", + aesMaxKeyLength + ); + // CS-ENFORCE-SINGLE + } + } catch (final NoSuchAlgorithmException e) { + LOGGER.error("AES encryption not supported (SG 1). ", e); + } + } + + void openSslWarnings(final Settings settings) { + if (!OpenSearchSecuritySSLPlugin.OPENSSL_SUPPORTED + && OpenSsl.isAvailable() + && (settings.getAsBoolean(SECURITY_SSL_HTTP_ENABLE_OPENSSL_IF_AVAILABLE, true) + || settings.getAsBoolean(SECURITY_SSL_TRANSPORT_ENABLE_OPENSSL_IF_AVAILABLE, true))) { + if (PlatformDependent.javaVersion() < 12) { + LOGGER.warn( + "Support for OpenSSL with Java 11 or prior versions require using Netty allocator. Set " + + "'opensearch.unsafe.use_netty_default_allocator' system property to true" + ); + } else { + LOGGER.warn("Support for OpenSSL with Java 12+ has been removed from OpenSearch Security. Using JDK SSL instead."); + } + } + if (OpenSearchSecuritySSLPlugin.OPENSSL_SUPPORTED && OpenSsl.isAvailable()) { + LOGGER.info("OpenSSL {} ({}) available", OpenSsl.versionString(), OpenSsl.version()); + + if (OpenSsl.version() < 0x10002000L) { + LOGGER.warn( + "Outdated OpenSSL version detected. You should update to 1.0.2k or later. Currently installed: {}", + OpenSsl.versionString() + ); + } + + if (!OpenSsl.supportsHostnameValidation()) { + LOGGER.warn( + "Your OpenSSL version {} does not support hostname verification. You should update to 1.0.2k or later.", + OpenSsl.versionString() + ); + } + + LOGGER.debug("OpenSSL available ciphers {}", OpenSsl.availableOpenSslCipherSuites()); + } else { + LOGGER.warn( + "OpenSSL not available (this is not an error, we simply fallback to built-in JDK SSL) because of {}", + OpenSsl.unavailabilityCause() + ); + } + } + +} diff --git a/src/main/java/org/opensearch/security/ssl/config/CertType.java b/src/main/java/org/opensearch/security/ssl/config/CertType.java new file mode 100644 index 0000000000..09a8dcfae9 --- /dev/null +++ b/src/main/java/org/opensearch/security/ssl/config/CertType.java @@ -0,0 +1,33 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.ssl.config; + +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_HTTP_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_TRANSPORT_CLIENT_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_TRANSPORT_PREFIX; + +public enum CertType { + HTTP(SSL_HTTP_PREFIX), + TRANSPORT(SSL_TRANSPORT_PREFIX), + TRANSPORT_CLIENT(SSL_TRANSPORT_CLIENT_PREFIX); + + private final String sslConfigPrefix; + + private CertType(String sslConfigPrefix) { + this.sslConfigPrefix = sslConfigPrefix; + } + + public String sslConfigPrefix() { + return sslConfigPrefix; + } + +} diff --git a/src/main/java/org/opensearch/security/ssl/config/Certificate.java b/src/main/java/org/opensearch/security/ssl/config/Certificate.java new file mode 100644 index 0000000000..534148db57 --- /dev/null +++ b/src/main/java/org/opensearch/security/ssl/config/Certificate.java @@ -0,0 +1,188 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.ssl.config; + +import java.lang.reflect.Method; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; + +import com.google.common.collect.ImmutableList; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.bouncycastle.asn1.ASN1InputStream; +import org.bouncycastle.asn1.ASN1Object; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.ASN1String; +import org.bouncycastle.asn1.ASN1TaggedObject; + +public class Certificate { + + private final static Logger LOGGER = LogManager.getLogger(Certificate.class); + + private final X509Certificate certificate; + + private final String format; + + private final String alias; + + private final boolean hasKey; + + public Certificate(final X509Certificate certificate, final boolean hasKey) { + this(certificate, "pem", null, hasKey); + } + + public Certificate(final X509Certificate certificate, final String format, final String alias, final boolean hasKey) { + this.certificate = certificate; + this.format = format; + this.alias = alias; + this.hasKey = hasKey; + } + + public X509Certificate x509Certificate() { + return certificate; + } + + public String format() { + return format; + } + + public String alias() { + return alias; + } + + public boolean hasKey() { + return hasKey; + } + + public String subjectAlternativeNames() { + return loadSubjectAlternativeNames(); + } + + @Deprecated(since = "since JDK 21", forRemoval = true) + public String loadSubjectAlternativeNames() { + String san = ""; + try { + Collection> altNames = certificate != null && certificate.getSubjectAlternativeNames() != null + ? certificate.getSubjectAlternativeNames() + : null; + if (altNames != null) { + Comparator> comparator = Comparator.comparing((List altName) -> (Integer) altName.get(0)) + .thenComparing((List altName) -> (String) altName.get(1)); + + Set> sans = new TreeSet<>(comparator); + for (List altName : altNames) { + Integer type = (Integer) altName.get(0); + // otherName requires parsing to string + if (type == 0) { + List otherName = parseOtherName(altName); + if (otherName != null) { + sans.add(Arrays.asList(type, otherName)); + } + } else { + sans.add(altName); + } + } + san = sans.toString(); + } + } catch (CertificateParsingException e) { + LOGGER.error("Issue parsing SubjectAlternativeName:", e); + } + + return san; + } + + @Deprecated(since = "since JDK 21", forRemoval = true) + private List parseOtherName(List altName) { + if (altName.size() < 2) { + LOGGER.warn("Couldn't parse subject alternative names"); + return null; + } + try (final ASN1InputStream in = new ASN1InputStream((byte[]) altName.get(1))) { + final ASN1Primitive asn1Primitive = in.readObject(); + final ASN1Sequence sequence = ASN1Sequence.getInstance(asn1Primitive); + final ASN1ObjectIdentifier asn1ObjectIdentifier = ASN1ObjectIdentifier.getInstance(sequence.getObjectAt(0)); + final ASN1TaggedObject asn1TaggedObject = ASN1TaggedObject.getInstance(sequence.getObjectAt(1)); + Method getObjectMethod = getObjectMethod(); + ASN1Object maybeTaggedAsn1Primitive = (ASN1Primitive) getObjectMethod.invoke(asn1TaggedObject); + if (maybeTaggedAsn1Primitive instanceof ASN1TaggedObject) { + maybeTaggedAsn1Primitive = (ASN1Primitive) getObjectMethod.invoke(maybeTaggedAsn1Primitive); + } + if (maybeTaggedAsn1Primitive instanceof ASN1String) { + return ImmutableList.of(asn1ObjectIdentifier.getId(), maybeTaggedAsn1Primitive.toString()); + } else { + LOGGER.warn("Couldn't parse subject alternative names"); + return null; + } + } catch (final Exception ioe) { // catch all exception here since BC throws diff exceptions + throw new RuntimeException("Couldn't parse subject alternative names", ioe); + } + } + + static Method getObjectMethod() throws ClassNotFoundException, NoSuchMethodException { + Class asn1TaggedObjectClass = Class.forName("org.bouncycastle.asn1.ASN1TaggedObject"); + try { + return asn1TaggedObjectClass.getMethod("getBaseObject"); + } catch (NoSuchMethodException ex) { + return asn1TaggedObjectClass.getMethod("getObject"); + } + } + + public String serialNumber() { + return certificate.getSerialNumber().toString(); + } + + public String subject() { + return certificate.getSubjectX500Principal() != null ? certificate.getSubjectX500Principal().getName() : null; + } + + public String issuer() { + return certificate.getIssuerX500Principal() != null ? certificate.getIssuerX500Principal().getName() : null; + } + + public String notAfter() { + return certificate.getNotAfter() != null ? certificate.getNotAfter().toInstant().toString() : null; + } + + public String notBefore() { + return certificate.getNotBefore() != null ? certificate.getNotBefore().toInstant().toString() : null; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Certificate that = (Certificate) o; + return hasKey == that.hasKey + && Objects.equals(certificate, that.certificate) + && Objects.equals(format, that.format) + && Objects.equals(alias, that.alias); + } + + @Override + public int hashCode() { + return Objects.hash(certificate, format, alias, hasKey); + } + + @Override + public String toString() { + return "Certificate{" + "format='" + format + '\'' + ", alias='" + alias + '\'' + ", hasKey=" + hasKey + '}'; + } +} diff --git a/src/main/java/org/opensearch/security/ssl/config/KeyStoreConfiguration.java b/src/main/java/org/opensearch/security/ssl/config/KeyStoreConfiguration.java new file mode 100644 index 0000000000..b1675f093a --- /dev/null +++ b/src/main/java/org/opensearch/security/ssl/config/KeyStoreConfiguration.java @@ -0,0 +1,201 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.ssl.config; + +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import javax.net.ssl.KeyManagerFactory; + +import com.google.common.collect.ImmutableList; + +import org.opensearch.OpenSearchException; +import org.opensearch.common.collect.Tuple; + +public interface KeyStoreConfiguration { + + List files(); + + List loadCertificates(); + + default KeyManagerFactory createKeyManagerFactory(boolean validateCertificates) { + final var keyStore = createKeyStore(); + if (validateCertificates) { + KeyStoreUtils.validateKeyStoreCertificates(keyStore.v1()); + } + return buildKeyManagerFactory(keyStore.v1(), keyStore.v2()); + } + + default KeyManagerFactory buildKeyManagerFactory(final KeyStore keyStore, final char[] password) { + try { + final var keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(keyStore, password); + return keyManagerFactory; + } catch (GeneralSecurityException e) { + throw new OpenSearchException("Failed to create KeyManagerFactory", e); + } + } + + Tuple createKeyStore(); + + final class JdkKeyStoreConfiguration implements KeyStoreConfiguration { + private final Path path; + + private final String type; + + private final String alias; + + private final char[] keyStorePassword; + + private final char[] keyPassword; + + public JdkKeyStoreConfiguration( + final Path path, + final String type, + final String alias, + final char[] keyStorePassword, + final char[] keyPassword + ) { + this.path = path; + this.type = type; + this.alias = alias; + this.keyStorePassword = keyStorePassword; + this.keyPassword = keyPassword; + } + + private void loadCertificateChain(final String alias, final KeyStore keyStore, final ImmutableList.Builder listBuilder) + throws KeyStoreException { + final var cc = keyStore.getCertificateChain(alias); + var first = true; + for (final var c : cc) { + if (c instanceof X509Certificate) { + listBuilder.add(new Certificate((X509Certificate) c, type, alias, first)); + first = false; + } + } + } + + @Override + public List loadCertificates() { + final var keyStore = KeyStoreUtils.loadKeyStore(path, type, keyStorePassword); + final var listBuilder = ImmutableList.builder(); + + try { + if (alias != null) { + if (keyStore.isKeyEntry(alias)) { + loadCertificateChain(alias, keyStore, listBuilder); + } + } else { + for (final var a : Collections.list(keyStore.aliases())) { + if (keyStore.isKeyEntry(a)) { + loadCertificateChain(a, keyStore, listBuilder); + } + } + } + final var list = listBuilder.build(); + if (list.isEmpty()) { + throw new OpenSearchException("The file " + path + " does not contain any certificates"); + } + return listBuilder.build(); + } catch (GeneralSecurityException e) { + throw new OpenSearchException("Couldn't load certificates from file " + path, e); + } + } + + @Override + public List files() { + return List.of(path); + } + + @Override + public Tuple createKeyStore() { + final var keyStore = KeyStoreUtils.newKeyStore(path, type, alias, keyStorePassword, keyPassword); + return Tuple.tuple(keyStore, keyPassword); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + JdkKeyStoreConfiguration that = (JdkKeyStoreConfiguration) o; + return Objects.equals(path, that.path) + && Objects.equals(type, that.type) + && Objects.equals(alias, that.alias) + && Objects.deepEquals(keyStorePassword, that.keyStorePassword) + && Objects.deepEquals(keyPassword, that.keyPassword); + } + + @Override + public int hashCode() { + return Objects.hash(path, type, alias, Arrays.hashCode(keyStorePassword), Arrays.hashCode(keyPassword)); + } + } + + final class PemKeyStoreConfiguration implements KeyStoreConfiguration { + + private final Path certificateChainPath; + + private final Path keyPath; + + private final char[] keyPassword; + + public PemKeyStoreConfiguration(final Path certificateChainPath, final Path keyPath, final char[] keyPassword) { + this.certificateChainPath = certificateChainPath; + this.keyPath = keyPath; + this.keyPassword = keyPassword; + } + + @Override + public List loadCertificates() { + final var certificates = KeyStoreUtils.x509Certificates(certificateChainPath); + final var listBuilder = ImmutableList.builder(); + listBuilder.add(new Certificate(certificates[0], true)); + for (int i = 1; i < certificates.length; i++) { + listBuilder.add(new Certificate(certificates[i], false)); + } + return listBuilder.build(); + } + + @Override + public List files() { + return List.of(certificateChainPath, keyPath); + } + + @Override + public Tuple createKeyStore() { + final var keyStore = KeyStoreUtils.newKeyStoreFromPem(certificateChainPath, keyPath, keyPassword); + return Tuple.tuple(keyStore, keyPassword); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PemKeyStoreConfiguration that = (PemKeyStoreConfiguration) o; + return Objects.equals(certificateChainPath, that.certificateChainPath) + && Objects.equals(keyPath, that.keyPath) + && Objects.deepEquals(keyPassword, that.keyPassword); + } + + @Override + public int hashCode() { + return Objects.hash(certificateChainPath, keyPath, Arrays.hashCode(keyPassword)); + } + } + +} diff --git a/src/main/java/org/opensearch/security/ssl/config/KeyStoreUtils.java b/src/main/java/org/opensearch/security/ssl/config/KeyStoreUtils.java new file mode 100644 index 0000000000..7c063bd312 --- /dev/null +++ b/src/main/java/org/opensearch/security/ssl/config/KeyStoreUtils.java @@ -0,0 +1,218 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.ssl.config; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; +import java.util.List; +import javax.crypto.NoSuchPaddingException; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLSessionContext; + +import org.opensearch.OpenSearchException; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.ssl.ApplicationProtocolNegotiator; +import io.netty.handler.ssl.SslContext; + +final class KeyStoreUtils { + + private final static class SecuritySslContext extends SslContext { + + private SecuritySslContext() {} + + @Override + public boolean isClient() { + throw new UnsupportedOperationException("Method isClient is not supported"); + } + + @Override + public List cipherSuites() { + throw new UnsupportedOperationException("Method cipherSuites is not supported"); + } + + @Override + public ApplicationProtocolNegotiator applicationProtocolNegotiator() { + throw new UnsupportedOperationException("Method applicationProtocolNegotiator is not supported"); + } + + @Override + public SSLEngine newEngine(ByteBufAllocator alloc) { + throw new UnsupportedOperationException("Method newEngine is not supported"); + } + + @Override + public SSLEngine newEngine(ByteBufAllocator alloc, String peerHost, int peerPort) { + throw new UnsupportedOperationException("Method newEngine is not supported"); + } + + @Override + public SSLSessionContext sessionContext() { + throw new UnsupportedOperationException("Method sessionContext is not supported"); + } + + public static X509Certificate[] toX509Certificates(final File file) { + try { + return SslContext.toX509Certificates(file); + } catch (CertificateException e) { + throw new OpenSearchException("Couldn't read SSL certificates from " + file, e); + } + } + + protected static PrivateKey toPrivateKey(File keyFile, String keyPassword) throws InvalidAlgorithmParameterException, + NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeySpecException, IOException, KeyException { + return SslContext.toPrivateKey(keyFile, keyPassword); + } + + } + + public static X509Certificate[] x509Certificates(final Path file) { + final var certificates = SecuritySslContext.toX509Certificates(file.toFile()); + if (certificates == null || certificates.length == 0) { + throw new OpenSearchException("Couldn't read SSL certificates from " + file); + } + return certificates; + } + + public static KeyStore loadTrustStore(final Path path, final String type, final String alias, final char[] password) { + try { + var keyStore = loadKeyStore(path, type, password); + if (alias != null) { + if (!keyStore.isCertificateEntry(alias)) { + throw new OpenSearchException("Alias " + alias + " does not contain a certificate entry"); + } + final var aliasCertificate = (X509Certificate) keyStore.getCertificate(alias); + if (aliasCertificate == null) { + throw new OpenSearchException("Couldn't find SSL certificate for alias " + alias); + } + keyStore = newKeyStore(); + keyStore.setCertificateEntry(alias, aliasCertificate); + } + return keyStore; + } catch (Exception e) { + throw new OpenSearchException("Failed to load trust store from " + path, e); + } + } + + public static KeyStore newTrustStoreFromPem(final Path pemFile) { + try { + final var certs = x509Certificates(pemFile); + final var keyStore = newKeyStore(); + for (int i = 0; i < certs.length; i++) { + final var c = certs[i]; + keyStore.setCertificateEntry("os-sec-plugin-pem-cert-" + i, c); + } + return keyStore; + } catch (final Exception e) { + throw new OpenSearchException("Failed to load SSL certificates from " + pemFile, e); + } + } + + private static KeyStore newKeyStore() throws KeyStoreException, CertificateException, IOException, NoSuchAlgorithmException { + final var keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(null, null); + return keyStore; + } + + public static void validateKeyStoreCertificates(final KeyStore keyStore) { + try { + final var aliases = keyStore.aliases(); + while (aliases.hasMoreElements()) { + final var a = aliases.nextElement(); + if (keyStore.isCertificateEntry(a)) { + final var c = (X509Certificate) keyStore.getCertificate(a); + if (c == null) { + throw new CertificateException("Alias " + a + " does not contain a certificate entry"); + } + c.checkValidity(); + } else if (keyStore.isKeyEntry(a)) { + final var cc = keyStore.getCertificateChain(a); + if (cc == null) { + throw new CertificateException("Alias " + a + " does not contain a certificate chain"); + } + for (final var c : cc) { + ((X509Certificate) c).checkValidity(); + } + } + } + } catch (KeyStoreException e) { + throw new OpenSearchException("Couldn't load keys store", e); + } catch (CertificateException e) { + throw new OpenSearchException("Invalid certificates", e); + } + } + + public static KeyStore loadKeyStore(final Path path, final String type, final char[] password) { + try { + final var keyStore = KeyStore.getInstance(type); + try (final var in = Files.newInputStream(path)) { + keyStore.load(in, password); + return keyStore; + } catch (IOException e) { + throw new RuntimeException(e); + } + } catch (Exception e) { + throw new OpenSearchException("Failed to load keystore from " + path, e); + } + } + + public static KeyStore newKeyStore( + final Path path, + final String type, + final String alias, + final char[] password, + final char[] keyPassword + ) { + try { + var keyStore = loadKeyStore(path, type, password); + if (alias != null) { + if (!keyStore.isKeyEntry(alias)) { + throw new CertificateException("Couldn't find SSL key for alias " + alias); + } + final var certificateChain = keyStore.getCertificateChain(alias); + if (certificateChain == null) { + throw new CertificateException("Couldn't find certificate chain for alias " + alias); + } + final var key = keyStore.getKey(alias, keyPassword); + keyStore = newKeyStore(); + keyStore.setKeyEntry(alias, key, keyPassword, certificateChain); + } + return keyStore; + } catch (final Exception e) { + throw new OpenSearchException("Failed to load key store from " + path, e); + } + } + + public static KeyStore newKeyStoreFromPem(final Path certificateChainPath, final Path keyPath, final char[] keyPassword) { + try { + final var certificateChain = x509Certificates(certificateChainPath); + final var keyStore = newKeyStore(); + final var key = SecuritySslContext.toPrivateKey(keyPath.toFile(), keyPassword != null ? new String(keyPassword) : null); + keyStore.setKeyEntry("key", key, keyPassword, certificateChain); + return keyStore; + } catch (Exception e) { + throw new OpenSearchException("Failed read key from " + keyPath, e); + } + } + +} diff --git a/src/main/java/org/opensearch/security/ssl/config/SslCertificatesLoader.java b/src/main/java/org/opensearch/security/ssl/config/SslCertificatesLoader.java new file mode 100644 index 0000000000..a3f0c39eed --- /dev/null +++ b/src/main/java/org/opensearch/security/ssl/config/SslCertificatesLoader.java @@ -0,0 +1,171 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.ssl.config; + +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.security.KeyStore; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.OpenSearchException; +import org.opensearch.common.collect.Tuple; +import org.opensearch.common.settings.SecureSetting; +import org.opensearch.common.settings.Settings; +import org.opensearch.env.Environment; + +import static org.opensearch.security.ssl.SecureSSLSettings.SECURE_SUFFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.DEFAULT_STORE_PASSWORD; +import static org.opensearch.security.ssl.util.SSLConfigConstants.KEYSTORE_ALIAS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.KEYSTORE_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.KEYSTORE_KEY_PASSWORD; +import static org.opensearch.security.ssl.util.SSLConfigConstants.KEYSTORE_PASSWORD; +import static org.opensearch.security.ssl.util.SSLConfigConstants.KEYSTORE_TYPE; +import static org.opensearch.security.ssl.util.SSLConfigConstants.PEM_CERT_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.PEM_KEY_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.PEM_KEY_PASSWORD; +import static org.opensearch.security.ssl.util.SSLConfigConstants.PEM_TRUSTED_CAS_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.TRUSTSTORE_ALIAS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.TRUSTSTORE_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.TRUSTSTORE_PASSWORD; +import static org.opensearch.security.ssl.util.SSLConfigConstants.TRUSTSTORE_TYPE; + +public class SslCertificatesLoader { + + private final static Logger LOGGER = LogManager.getLogger(SslCertificatesLoader.class); + + private final String sslConfigSuffix; + + private final String fullSslConfigSuffix; + + public SslCertificatesLoader(final String sslConfigSuffix) { + this(sslConfigSuffix, null); + } + + public SslCertificatesLoader(final String sslConfigSuffix, final String extendedSslConfigSuffix) { + this.sslConfigSuffix = sslConfigSuffix; + this.fullSslConfigSuffix = extendedSslConfigSuffix != null ? sslConfigSuffix + extendedSslConfigSuffix : sslConfigSuffix; + } + + public Tuple loadConfiguration(final Environment environment) { + final var settings = environment.settings(); + final var sslConfigSettings = settings.getByPrefix(fullSslConfigSuffix); + if (settings.hasValue(sslConfigSuffix + KEYSTORE_FILEPATH)) { + return Tuple.tuple( + environment.settings().hasValue(sslConfigSuffix + TRUSTSTORE_FILEPATH) + ? buildJdkTrustStoreConfiguration( + sslConfigSettings, + environment, + resolvePassword(sslConfigSuffix + TRUSTSTORE_PASSWORD, settings, DEFAULT_STORE_PASSWORD) + ) + : TrustStoreConfiguration.EMPTY_CONFIGURATION, + buildJdkKeyStoreConfiguration( + sslConfigSettings, + environment, + resolvePassword(sslConfigSuffix + KEYSTORE_PASSWORD, settings, DEFAULT_STORE_PASSWORD), + resolvePassword(fullSslConfigSuffix + KEYSTORE_KEY_PASSWORD, settings, DEFAULT_STORE_PASSWORD) + ) + ); + } else { + return Tuple.tuple( + sslConfigSettings.hasValue(PEM_TRUSTED_CAS_FILEPATH) + ? new TrustStoreConfiguration.PemTrustStoreConfiguration( + resolvePath(sslConfigSettings.get(PEM_TRUSTED_CAS_FILEPATH), environment) + ) + : TrustStoreConfiguration.EMPTY_CONFIGURATION, + buildPemKeyStoreConfiguration( + sslConfigSettings, + environment, + resolvePassword(fullSslConfigSuffix + PEM_KEY_PASSWORD, settings, null) + ) + ); + } + } + + private char[] resolvePassword(final String legacyPasswordSettings, final Settings settings, final String defaultPassword) { + final var securePasswordSetting = String.format("%s%s", legacyPasswordSettings, SECURE_SUFFIX); + final var securePassword = SecureSetting.secureString(securePasswordSetting, null).get(settings); + final var legacyPassword = settings.get(legacyPasswordSettings, defaultPassword); + if (!securePassword.isEmpty() && legacyPassword != null && !legacyPassword.equals(defaultPassword)) { + throw new OpenSearchException("One of " + legacyPasswordSettings + " or " + securePasswordSetting + " must be set not both"); + } + if (!securePassword.isEmpty()) { + return securePassword.getChars(); + } else { + if (legacyPassword != null) { + LOGGER.warn( + "Setting [{}] has a secure counterpart [{}] which should be used instead - allowing for legacy SSL setups", + legacyPasswordSettings, + securePasswordSetting + ); + return legacyPassword.toCharArray(); + } + } + return null; + } + + private KeyStoreConfiguration.JdkKeyStoreConfiguration buildJdkKeyStoreConfiguration( + final Settings settings, + final Environment environment, + final char[] keyStorePassword, + final char[] keyPassword + ) { + return new KeyStoreConfiguration.JdkKeyStoreConfiguration( + resolvePath(environment.settings().get(sslConfigSuffix + KEYSTORE_FILEPATH), environment), + environment.settings().get(sslConfigSuffix + KEYSTORE_TYPE, KeyStore.getDefaultType()), + settings.get(KEYSTORE_ALIAS, null), + keyStorePassword, + keyPassword + ); + } + + private TrustStoreConfiguration.JdkTrustStoreConfiguration buildJdkTrustStoreConfiguration( + final Settings settings, + final Environment environment, + final char[] trustStorePassword + ) { + return new TrustStoreConfiguration.JdkTrustStoreConfiguration( + resolvePath(environment.settings().get(sslConfigSuffix + TRUSTSTORE_FILEPATH), environment), + environment.settings().get(sslConfigSuffix + TRUSTSTORE_TYPE, KeyStore.getDefaultType()), + settings.get(TRUSTSTORE_ALIAS, null), + trustStorePassword + ); + } + + private KeyStoreConfiguration.PemKeyStoreConfiguration buildPemKeyStoreConfiguration( + final Settings settings, + final Environment environment, + final char[] pemKeyPassword + ) { + return new KeyStoreConfiguration.PemKeyStoreConfiguration( + resolvePath(settings.get(PEM_CERT_FILEPATH), environment), + resolvePath(settings.get(PEM_KEY_FILEPATH), environment), + pemKeyPassword + ); + } + + private Path resolvePath(final String filePath, final Environment environment) { + final var path = environment.configDir().resolve(Path.of(filePath)); + if (Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) { + throw new OpenSearchException(filePath + " - is a directory"); + } + if (!Files.isReadable(path)) { + throw new OpenSearchException( + "Unable to read the file " + filePath + ". Please make sure this files exists and is readable regarding to permissions" + ); + } + return path; + } + +} diff --git a/src/main/java/org/opensearch/security/ssl/config/SslParameters.java b/src/main/java/org/opensearch/security/ssl/config/SslParameters.java new file mode 100644 index 0000000000..eef14cea0a --- /dev/null +++ b/src/main/java/org/opensearch/security/ssl/config/SslParameters.java @@ -0,0 +1,197 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.ssl.config; + +import java.security.NoSuchAlgorithmException; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.net.ssl.SSLContext; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.OpenSearchException; +import org.opensearch.OpenSearchSecurityException; +import org.opensearch.common.settings.Settings; + +import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.OpenSsl; +import io.netty.handler.ssl.SslProvider; + +import static org.opensearch.security.ssl.util.SSLConfigConstants.ALLOWED_OPENSSL_HTTP_PROTOCOLS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.ALLOWED_OPENSSL_HTTP_PROTOCOLS_PRIOR_OPENSSL_1_1_1_BETA_9; +import static org.opensearch.security.ssl.util.SSLConfigConstants.ALLOWED_OPENSSL_TRANSPORT_PROTOCOLS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.ALLOWED_OPENSSL_TRANSPORT_PROTOCOLS_PRIOR_OPENSSL_1_1_1_BETA_9; +import static org.opensearch.security.ssl.util.SSLConfigConstants.ALLOWED_SSL_CIPHERS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.ALLOWED_SSL_PROTOCOLS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.CLIENT_AUTH_MODE; +import static org.opensearch.security.ssl.util.SSLConfigConstants.ENABLED_CIPHERS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.ENABLED_PROTOCOLS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.ENABLE_OPENSSL_IF_AVAILABLE; +import static org.opensearch.security.ssl.util.SSLConfigConstants.OPENSSL_1_1_1_BETA_9; +import static org.opensearch.security.ssl.util.SSLConfigConstants.OPENSSL_AVAILABLE; + +public class SslParameters { + + private final SslProvider provider; + + private final ClientAuth clientAuth; + + private final List protocols; + + private final List ciphers; + + private SslParameters(SslProvider provider, final ClientAuth clientAuth, List protocols, List ciphers) { + this.provider = provider; + this.ciphers = ciphers; + this.protocols = protocols; + this.clientAuth = clientAuth; + } + + public ClientAuth clientAuth() { + return clientAuth; + } + + public SslProvider provider() { + return provider; + } + + public List allowedCiphers() { + return ciphers; + } + + public List allowedProtocols() { + return protocols; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SslParameters that = (SslParameters) o; + return provider == that.provider && Objects.equals(ciphers, that.ciphers) && Objects.equals(protocols, that.protocols); + } + + @Override + public int hashCode() { + return Objects.hash(provider, ciphers, protocols); + } + + public static Loader loader(final Settings sslConfigSettings) { + return new Loader(sslConfigSettings); + } + + public static final class Loader { + + private final static Logger LOGGER = LogManager.getLogger(SslParameters.class); + + private final Settings sslConfigSettings; + + public Loader(final Settings sslConfigSettings) { + this.sslConfigSettings = sslConfigSettings; + } + + private SslProvider provider(final Settings settings) { + final var useOpenSslIfAvailable = settings.getAsBoolean(ENABLE_OPENSSL_IF_AVAILABLE, true); + if (OPENSSL_AVAILABLE && useOpenSslIfAvailable) { + return SslProvider.OPENSSL; + } else { + return SslProvider.JDK; + } + } + + private List protocols(final SslProvider provider, final Settings settings, boolean http) { + final var allowedProtocols = settings.getAsList(ENABLED_PROTOCOLS, List.of(ALLOWED_SSL_PROTOCOLS)); + if (provider == SslProvider.OPENSSL) { + final String[] supportedProtocols; + if (OpenSsl.version() > OPENSSL_1_1_1_BETA_9) { + supportedProtocols = http ? ALLOWED_OPENSSL_HTTP_PROTOCOLS : ALLOWED_OPENSSL_TRANSPORT_PROTOCOLS; + } else { + supportedProtocols = http + ? ALLOWED_OPENSSL_HTTP_PROTOCOLS_PRIOR_OPENSSL_1_1_1_BETA_9 + : ALLOWED_OPENSSL_TRANSPORT_PROTOCOLS_PRIOR_OPENSSL_1_1_1_BETA_9; + } + return openSslProtocols(allowedProtocols, supportedProtocols); + } else { + return jdkProtocols(allowedProtocols); + } + } + + private List openSslProtocols(final List allowedSslProtocols, final String... supportedProtocols) { + LOGGER.debug("OpenSSL supports the following {} protocols {}", supportedProtocols.length, supportedProtocols); + return Stream.of(supportedProtocols).filter(allowedSslProtocols::contains).collect(Collectors.toList()); + } + + private List jdkProtocols(final List allowedSslProtocols) { + try { + final var supportedProtocols = SSLContext.getDefault().getDefaultSSLParameters().getProtocols(); + LOGGER.debug("JVM supports the following {} protocols {}", supportedProtocols.length, supportedProtocols); + return Stream.of(supportedProtocols).filter(allowedSslProtocols::contains).collect(Collectors.toList()); + } catch (final NoSuchAlgorithmException e) { + throw new OpenSearchException("Unable to determine supported protocols", e); + } + } + + private List ciphers(final SslProvider provider, final Settings settings) { + final var allowed = settings.getAsList(ENABLED_CIPHERS, List.of(ALLOWED_SSL_CIPHERS)); + final Stream allowedCiphers; + if (provider == SslProvider.OPENSSL) { + LOGGER.debug( + "OpenSSL {} supports the following ciphers (java-style) {}", + OpenSsl.versionString(), + OpenSsl.availableJavaCipherSuites() + ); + LOGGER.debug( + "OpenSSL {} supports the following ciphers (openssl-style) {}", + OpenSsl.versionString(), + OpenSsl.availableOpenSslCipherSuites() + ); + allowedCiphers = allowed.stream().filter(OpenSsl::isCipherSuiteAvailable); + } else { + try { + final var supportedCiphers = SSLContext.getDefault().getDefaultSSLParameters().getCipherSuites(); + LOGGER.debug("JVM supports the following {} ciphers {}", supportedCiphers.length, supportedCiphers); + allowedCiphers = Stream.of(supportedCiphers).filter(allowed::contains); + } catch (final NoSuchAlgorithmException e) { + throw new OpenSearchException("Unable to determine ciphers protocols", e); + } + } + return allowedCiphers.sorted(String::compareTo).collect(Collectors.toList()); + } + + public SslParameters load(final boolean http) { + final var clientAuth = http + ? ClientAuth.valueOf(sslConfigSettings.get(CLIENT_AUTH_MODE, ClientAuth.OPTIONAL.name()).toUpperCase(Locale.ROOT)) + : ClientAuth.REQUIRE; + + final var provider = provider(sslConfigSettings); + final var sslParameters = new SslParameters( + provider, + clientAuth, + protocols(provider, sslConfigSettings, http), + ciphers(provider, sslConfigSettings) + ); + if (sslParameters.allowedProtocols().isEmpty()) { + throw new OpenSearchSecurityException("No ssl protocols for " + (http ? "HTTP" : "Transport") + " layer"); + } + if (sslParameters.allowedCiphers().isEmpty()) { + throw new OpenSearchSecurityException("No valid cipher suites for " + (http ? "HTTP" : "Transport") + " layer"); + } + return sslParameters; + } + + } + +} diff --git a/src/main/java/org/opensearch/security/ssl/config/TrustStoreConfiguration.java b/src/main/java/org/opensearch/security/ssl/config/TrustStoreConfiguration.java new file mode 100644 index 0000000000..4965aa3216 --- /dev/null +++ b/src/main/java/org/opensearch/security/ssl/config/TrustStoreConfiguration.java @@ -0,0 +1,185 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.ssl.config; + +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.net.ssl.TrustManagerFactory; + +import com.google.common.collect.ImmutableList; + +import org.opensearch.OpenSearchException; + +public interface TrustStoreConfiguration { + + TrustStoreConfiguration EMPTY_CONFIGURATION = new TrustStoreConfiguration() { + @Override + public Path file() { + return null; + } + + @Override + public List loadCertificates() { + return List.of(); + } + + @Override + public KeyStore createTrustStore() { + return null; + } + + @Override + public TrustManagerFactory createTrustManagerFactory(boolean validateCertificates) { + return null; + } + }; + + Path file(); + + List loadCertificates(); + + default TrustManagerFactory createTrustManagerFactory(boolean validateCertificates) { + final var trustStore = createTrustStore(); + if (validateCertificates) { + KeyStoreUtils.validateKeyStoreCertificates(trustStore); + } + return buildTrustManagerFactory(trustStore); + } + + default TrustManagerFactory buildTrustManagerFactory(final KeyStore keyStore) { + try { + final var trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(keyStore); + return trustManagerFactory; + } catch (GeneralSecurityException e) { + throw new OpenSearchException("Couldn't initialize TrustManagerFactory", e); + } + } + + KeyStore createTrustStore(); + + final class JdkTrustStoreConfiguration implements TrustStoreConfiguration { + + private final Path path; + + private final String type; + + private final String alias; + + private final char[] password; + + public JdkTrustStoreConfiguration(final Path path, final String type, final String alias, final char[] password) { + this.path = path; + this.type = type; + this.alias = alias; + this.password = password; + } + + @Override + public List loadCertificates() { + final var keyStore = KeyStoreUtils.loadKeyStore(path, type, password); + final var listBuilder = ImmutableList.builder(); + try { + if (alias != null) { + listBuilder.add(new Certificate((X509Certificate) keyStore.getCertificate(alias), type, alias, false)); + } else { + for (final var a : Collections.list(keyStore.aliases())) { + if (!keyStore.isCertificateEntry(a)) continue; + final var c = keyStore.getCertificate(a); + if (c instanceof X509Certificate) { + listBuilder.add(new Certificate((X509Certificate) c, type, a, false)); + } + } + } + final var list = listBuilder.build(); + if (list.isEmpty()) { + throw new OpenSearchException("The file " + path + " does not contain any certificates"); + } + return listBuilder.build(); + } catch (GeneralSecurityException e) { + throw new OpenSearchException("Couldn't load certificates from file " + path, e); + } + } + + @Override + public Path file() { + return path; + } + + @Override + public KeyStore createTrustStore() { + return KeyStoreUtils.loadTrustStore(path, type, alias, password); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + JdkTrustStoreConfiguration that = (JdkTrustStoreConfiguration) o; + return Objects.equals(path, that.path) + && Objects.equals(type, that.type) + && Objects.equals(alias, that.alias) + && Objects.deepEquals(password, that.password); + } + + @Override + public int hashCode() { + return Objects.hash(path, type, alias, Arrays.hashCode(password)); + } + } + + final class PemTrustStoreConfiguration implements TrustStoreConfiguration { + + private final Path path; + + public PemTrustStoreConfiguration(final Path path) { + this.path = path; + } + + @Override + public List loadCertificates() { + return Stream.of(KeyStoreUtils.x509Certificates(path)).map(c -> new Certificate(c, false)).collect(Collectors.toList()); + } + + @Override + public Path file() { + return path; + } + + @Override + public KeyStore createTrustStore() { + return KeyStoreUtils.newTrustStoreFromPem(path); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PemTrustStoreConfiguration that = (PemTrustStoreConfiguration) o; + return Objects.equals(path, that.path); + } + + @Override + public int hashCode() { + return Objects.hashCode(path); + } + } + +} diff --git a/src/main/java/org/opensearch/security/ssl/rest/SecuritySSLInfoAction.java b/src/main/java/org/opensearch/security/ssl/rest/SecuritySSLInfoAction.java index b9f9e949ec..203a0c7965 100644 --- a/src/main/java/org/opensearch/security/ssl/rest/SecuritySSLInfoAction.java +++ b/src/main/java/org/opensearch/security/ssl/rest/SecuritySSLInfoAction.java @@ -35,11 +35,13 @@ import org.opensearch.rest.BaseRestHandler; import org.opensearch.rest.BytesRestResponse; import org.opensearch.rest.RestChannel; -import org.opensearch.rest.RestController; import org.opensearch.rest.RestRequest; import org.opensearch.rest.RestRequest.Method; import org.opensearch.security.filter.SecurityRequestFactory; -import org.opensearch.security.ssl.SecurityKeyStore; +import org.opensearch.security.ssl.SslConfiguration; +import org.opensearch.security.ssl.SslSettingsManager; +import org.opensearch.security.ssl.config.CertType; +import org.opensearch.security.ssl.config.SslParameters; import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.security.ssl.util.SSLRequestHelper; import org.opensearch.security.ssl.util.SSLRequestHelper.SSLInfo; @@ -50,7 +52,7 @@ public class SecuritySSLInfoAction extends BaseRestHandler { private static final List routes = Collections.singletonList(new Route(Method.GET, "/_opendistro/_security/sslinfo")); private final Logger log = LogManager.getLogger(this.getClass()); - private final SecurityKeyStore sks; + private final SslSettingsManager sslSettingsManager; final PrincipalExtractor principalExtractor; private final Path configPath; private final Settings settings; @@ -58,13 +60,12 @@ public class SecuritySSLInfoAction extends BaseRestHandler { public SecuritySSLInfoAction( final Settings settings, final Path configPath, - final RestController controller, - final SecurityKeyStore sks, + final SslSettingsManager sslSettingsManager, final PrincipalExtractor principalExtractor ) { super(); this.settings = settings; - this.sks = sks; + this.sslSettingsManager = sslSettingsManager; this.principalExtractor = principalExtractor; this.configPath = configPath; } @@ -103,13 +104,15 @@ public void accept(RestChannel channel) throws Exception { if (showDn == Boolean.TRUE) { builder.field( "peer_certificates_list", - certs == null ? null : Arrays.stream(certs).map(c -> c.getSubjectDN().getName()).collect(Collectors.toList()) + certs == null + ? null + : Arrays.stream(certs).map(c -> c.getSubjectX500Principal().getName()).collect(Collectors.toList()) ); builder.field( "local_certificates_list", localCerts == null ? null - : Arrays.stream(localCerts).map(c -> c.getSubjectDN().getName()).collect(Collectors.toList()) + : Arrays.stream(localCerts).map(c -> c.getSubjectX500Principal().getName()).collect(Collectors.toList()) ); } @@ -122,9 +125,27 @@ public void accept(RestChannel channel) throws Exception { builder.field("ssl_openssl_non_available_cause", openSslUnavailCause == null ? "" : openSslUnavailCause.toString()); builder.field("ssl_openssl_supports_key_manager_factory", OpenSsl.supportsKeyManagerFactory()); builder.field("ssl_openssl_supports_hostname_validation", OpenSsl.supportsHostnameValidation()); - builder.field("ssl_provider_http", sks.getHTTPProviderName()); - builder.field("ssl_provider_transport_server", sks.getTransportServerProviderName()); - builder.field("ssl_provider_transport_client", sks.getTransportClientProviderName()); + builder.field( + "ssl_provider_http", + sslSettingsManager.sslConfiguration(CertType.HTTP) + .map(SslConfiguration::sslParameters) + .map(SslParameters::provider) + .orElse(null) + ); + builder.field( + "ssl_provider_transport_server", + sslSettingsManager.sslConfiguration(CertType.TRANSPORT) + .map(SslConfiguration::sslParameters) + .map(SslParameters::provider) + .orElse(null) + ); + builder.field( + "ssl_provider_transport_client", + sslSettingsManager.sslConfiguration(CertType.TRANSPORT_CLIENT) + .map(SslConfiguration::sslParameters) + .map(SslParameters::provider) + .orElse(null) + ); builder.endObject(); response = new BytesRestResponse(RestStatus.OK, builder); diff --git a/src/main/java/org/opensearch/security/ssl/util/SSLConfigConstants.java b/src/main/java/org/opensearch/security/ssl/util/SSLConfigConstants.java index a3b9348496..dfc9ae567e 100644 --- a/src/main/java/org/opensearch/security/ssl/util/SSLConfigConstants.java +++ b/src/main/java/org/opensearch/security/ssl/util/SSLConfigConstants.java @@ -22,9 +22,53 @@ import java.util.List; import org.opensearch.common.settings.Settings; +import org.opensearch.security.ssl.OpenSearchSecuritySSLPlugin; + +import io.netty.handler.ssl.OpenSsl; public final class SSLConfigConstants { + public static final String SSL_PREFIX = "plugins.security.ssl."; + + public static final String HTTP_SETTINGS = "http"; + + public static final String TRANSPORT_SETTINGS = "transport"; + + public static final String SSL_HTTP_PREFIX = SSL_PREFIX + HTTP_SETTINGS + "."; + + public static final String SSL_TRANSPORT_PREFIX = SSL_PREFIX + TRANSPORT_SETTINGS + "."; + + public static final String SSL_TRANSPORT_SERVER_EXTENDED_PREFIX = "server."; + + public static final String SSL_TRANSPORT_CLIENT_EXTENDED_PREFIX = "client."; + + public static final String SSL_TRANSPORT_CLIENT_PREFIX = SSL_PREFIX + TRANSPORT_SETTINGS + SSL_TRANSPORT_CLIENT_EXTENDED_PREFIX; + + public static final String ENABLED = "enabled"; + + public static final String CLIENT_AUTH_MODE = "clientauth_mode"; + + public static final String KEYSTORE_TYPE = "keystore_type"; + public static final String KEYSTORE_ALIAS = "keystore_alias"; + public static final String KEYSTORE_FILEPATH = "keystore_filepath"; + public static final String KEYSTORE_PASSWORD = "keystore_password"; + public static final String KEYSTORE_KEY_PASSWORD = "keystore_keypassword"; + + public static final String TRUSTSTORE_ALIAS = "truststore_alias"; + public static final String TRUSTSTORE_FILEPATH = "truststore_filepath"; + public static final String TRUSTSTORE_TYPE = "truststore_type"; + public static final String TRUSTSTORE_PASSWORD = "truststore_password"; + + public static final String PEM_KEY_FILEPATH = "pemkey_filepath"; + public static final String PEM_CERT_FILEPATH = "pemcert_filepath"; + public static final String PEM_TRUSTED_CAS_FILEPATH = "pemtrustedcas_filepath"; + public static final String EXTENDED_KEY_USAGE_ENABLED = "extended_key_usage_enabled"; + + public static final String ENABLE_OPENSSL_IF_AVAILABLE = "enable_openssl_if_available"; + public static final String ENABLED_PROTOCOLS = "enabled_protocols"; + public static final String ENABLED_CIPHERS = "enabled_ciphers"; + public static final String PEM_KEY_PASSWORD = "pemkey_password"; + public static final String SECURITY_SSL_HTTP_ENABLE_OPENSSL_IF_AVAILABLE = "plugins.security.ssl.http.enable_openssl_if_available"; public static final String SECURITY_SSL_HTTP_ENABLED = "plugins.security.ssl.http.enabled"; public static final boolean SECURITY_SSL_HTTP_ENABLED_DEFAULT = false; @@ -99,7 +143,19 @@ public final class SSLConfigConstants { public static final String JDK_TLS_REJECT_CLIENT_INITIATED_RENEGOTIATION = "jdk.tls.rejectClientInitiatedRenegotiation"; - private static final String[] _SECURE_SSL_PROTOCOLS = { "TLSv1.3", "TLSv1.2", "TLSv1.1" }; + public static final Long OPENSSL_1_1_1_BETA_9 = 0x10101009L; + + public static final String[] ALLOWED_SSL_PROTOCOLS = { "TLSv1.3", "TLSv1.2", "TLSv1.1" }; + + public static final String[] ALLOWED_OPENSSL_HTTP_PROTOCOLS = ALLOWED_SSL_PROTOCOLS; + + public static final String[] ALLOWED_OPENSSL_HTTP_PROTOCOLS_PRIOR_OPENSSL_1_1_1_BETA_9 = { "TLSv1.2", "TLSv1.1", "TLSv1" }; + + public static final String[] ALLOWED_OPENSSL_TRANSPORT_PROTOCOLS = ALLOWED_SSL_PROTOCOLS; + + public static final String[] ALLOWED_OPENSSL_TRANSPORT_PROTOCOLS_PRIOR_OPENSSL_1_1_1_BETA_9 = { "TLSv1.2", "TLSv1.1" }; + + public static final boolean OPENSSL_AVAILABLE = OpenSearchSecuritySSLPlugin.OPENSSL_SUPPORTED && OpenSsl.isAvailable(); public static String[] getSecureSSLProtocols(Settings settings, boolean http) { List configuredProtocols = null; @@ -116,11 +172,11 @@ public static String[] getSecureSSLProtocols(Settings settings, boolean http) { return configuredProtocols.toArray(new String[0]); } - return _SECURE_SSL_PROTOCOLS.clone(); + return ALLOWED_SSL_PROTOCOLS.clone(); } // @formatter:off - private static final String[] _SECURE_SSL_CIPHERS = { + public static final String[] ALLOWED_SSL_CIPHERS = { // TLS__WITH_ // Example (including unsafe ones) @@ -249,7 +305,7 @@ public static List getSecureSSLCiphers(Settings settings, boolean http) return configuredCiphers; } - return Collections.unmodifiableList(Arrays.asList(_SECURE_SSL_CIPHERS)); + return Collections.unmodifiableList(Arrays.asList(ALLOWED_SSL_CIPHERS)); } private SSLConfigConstants() { diff --git a/src/test/java/org/opensearch/security/ssl/CertificatesRule.java b/src/test/java/org/opensearch/security/ssl/CertificatesRule.java new file mode 100644 index 0000000000..a27de233dc --- /dev/null +++ b/src/test/java/org/opensearch/security/ssl/CertificatesRule.java @@ -0,0 +1,318 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.ssl; + +import java.io.IOException; +import java.math.BigInteger; +import java.nio.file.Path; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import java.util.List; + +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.rules.ExternalResource; +import org.junit.rules.TemporaryFolder; +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.style.RFC4519Style; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.ExtendedKeyUsage; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.asn1.x509.KeyPurposeId; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.cert.CertIOException; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; + +import org.opensearch.common.collect.Tuple; + +public class CertificatesRule extends ExternalResource { + + private final static BouncyCastleProvider BOUNCY_CASTLE_PROVIDER = new BouncyCastleProvider(); + + private final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + final static String DEFAULT_SUBJECT_NAME = "CN=some_access,OU=client,O=client,L=test,C=de"; + + private Path configRootFolder; + + private final String privateKeyPassword = RandomStringUtils.randomAlphabetic(10); + + private X509CertificateHolder caCertificateHolder; + + private X509CertificateHolder accessCertificateHolder; + + private PrivateKey accessCertificatePrivateKey; + + @Override + protected void before() throws Throwable { + super.before(); + temporaryFolder.create(); + configRootFolder = temporaryFolder.newFolder("esHome").toPath(); + final var keyPair = generateKeyPair(); + caCertificateHolder = generateCaCertificate(keyPair); + final var keyAndCertificate = generateAccessCertificate(keyPair); + accessCertificatePrivateKey = keyAndCertificate.v1(); + accessCertificateHolder = keyAndCertificate.v2(); + } + + @Override + protected void after() { + super.after(); + temporaryFolder.delete(); + } + + public Path configRootFolder() { + return configRootFolder; + } + + public String privateKeyPassword() { + return privateKeyPassword; + } + + public X509CertificateHolder caCertificateHolder() { + return caCertificateHolder; + } + + public X509CertificateHolder accessCertificateHolder() { + return accessCertificateHolder; + } + + public X509Certificate x509CaCertificate() throws CertificateException { + return toX509Certificate(caCertificateHolder); + } + + public X509Certificate x509AccessCertificate() throws CertificateException { + return toX509Certificate(accessCertificateHolder); + } + + public PrivateKey accessCertificatePrivateKey() { + return accessCertificatePrivateKey; + } + + public KeyPair generateKeyPair() throws NoSuchAlgorithmException { + KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", BOUNCY_CASTLE_PROVIDER); + generator.initialize(4096); + return generator.generateKeyPair(); + } + + public X509CertificateHolder generateCaCertificate(final KeyPair parentKeyPair) throws IOException, NoSuchAlgorithmException, + OperatorCreationException { + return generateCaCertificate(parentKeyPair, generateSerialNumber()); + } + + public X509CertificateHolder generateCaCertificate(final KeyPair parentKeyPair, final BigInteger serialNumber) throws IOException, + NoSuchAlgorithmException, OperatorCreationException { + final var startAndEndDate = generateStartAndEndDate(); + // CS-SUPPRESS-SINGLE: RegexpSingleline Extension should only be used sparingly to keep implementations as generic as possible + return createCertificateBuilder( + DEFAULT_SUBJECT_NAME, + DEFAULT_SUBJECT_NAME, + parentKeyPair.getPublic(), + parentKeyPair.getPublic(), + serialNumber, + startAndEndDate.v1(), + startAndEndDate.v2() + ).addExtension(Extension.basicConstraints, true, new BasicConstraints(true)) + .addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyCertSign | KeyUsage.cRLSign)) + .build(new JcaContentSignerBuilder("SHA256withRSA").setProvider(BOUNCY_CASTLE_PROVIDER).build(parentKeyPair.getPrivate())); + // CS-ENFORCE-SINGLE + } + + public Tuple generateAccessCertificate(final KeyPair parentKeyPair) throws NoSuchAlgorithmException, + IOException, OperatorCreationException { + final var startAndEndDate = generateStartAndEndDate(); + return generateAccessCertificate( + DEFAULT_SUBJECT_NAME, + DEFAULT_SUBJECT_NAME, + parentKeyPair, + generateSerialNumber(), + startAndEndDate.v1(), + startAndEndDate.v2(), + defaultSubjectAlternativeNames() + ); + } + + public Tuple generateAccessCertificate(final KeyPair parentKeyPair, final BigInteger serialNumber) + throws NoSuchAlgorithmException, IOException, OperatorCreationException { + final var startAdnEndDate = generateStartAndEndDate(); + return generateAccessCertificate( + DEFAULT_SUBJECT_NAME, + DEFAULT_SUBJECT_NAME, + parentKeyPair, + serialNumber, + startAdnEndDate.v1(), + startAdnEndDate.v2(), + defaultSubjectAlternativeNames() + ); + } + + public Tuple generateAccessCertificate( + final KeyPair parentKeyPair, + final Instant startDate, + final Instant endDate + ) throws NoSuchAlgorithmException, IOException, OperatorCreationException { + return generateAccessCertificate( + DEFAULT_SUBJECT_NAME, + DEFAULT_SUBJECT_NAME, + parentKeyPair, + generateSerialNumber(), + startDate, + endDate, + defaultSubjectAlternativeNames() + ); + } + + public Tuple generateAccessCertificate( + final KeyPair parentKeyPair, + final Instant startDate, + final Instant endDate, + List sans + ) throws NoSuchAlgorithmException, IOException, OperatorCreationException { + return generateAccessCertificate( + DEFAULT_SUBJECT_NAME, + DEFAULT_SUBJECT_NAME, + parentKeyPair, + generateSerialNumber(), + startDate, + endDate, + sans + ); + } + + public Tuple generateAccessCertificate( + final KeyPair parentKeyPair, + final String subject, + final String issuer + ) throws NoSuchAlgorithmException, IOException, OperatorCreationException { + final var startAndEndDate = generateStartAndEndDate(); + return generateAccessCertificate( + subject, + issuer, + parentKeyPair, + generateSerialNumber(), + startAndEndDate.v1(), + startAndEndDate.v2(), + defaultSubjectAlternativeNames() + ); + } + + public Tuple generateAccessCertificate(final KeyPair parentKeyPair, final List sans) + throws NoSuchAlgorithmException, IOException, OperatorCreationException { + final var startAndEndDate = generateStartAndEndDate(); + return generateAccessCertificate( + DEFAULT_SUBJECT_NAME, + DEFAULT_SUBJECT_NAME, + parentKeyPair, + generateSerialNumber(), + startAndEndDate.v1(), + startAndEndDate.v2(), + sans + ); + } + + public Tuple generateAccessCertificate( + final String subject, + final String issuer, + final KeyPair parentKeyPair, + final BigInteger serialNumber, + final Instant startDate, + final Instant endDate, + final List sans + ) throws NoSuchAlgorithmException, IOException, OperatorCreationException { + final var keyPair = generateKeyPair(); + // CS-SUPPRESS-SINGLE: RegexpSingleline Extension should only be used sparingly to keep implementations as generic as possible + final var certificate = createCertificateBuilder( + subject, + issuer, + keyPair.getPublic(), + parentKeyPair.getPublic(), + serialNumber, + startDate, + endDate + ).addExtension(Extension.basicConstraints, true, new BasicConstraints(false)) + .addExtension( + Extension.keyUsage, + true, + new KeyUsage(KeyUsage.digitalSignature | KeyUsage.nonRepudiation | KeyUsage.keyEncipherment) + ) + .addExtension(Extension.extendedKeyUsage, true, new ExtendedKeyUsage(KeyPurposeId.id_kp_clientAuth)) + .addExtension(Extension.subjectAlternativeName, false, new DERSequence(sans.toArray(sans.toArray(new ASN1Encodable[0])))) + .build(new JcaContentSignerBuilder("SHA256withRSA").setProvider(BOUNCY_CASTLE_PROVIDER).build(parentKeyPair.getPrivate())); + // CS-ENFORCE-SINGLE + return Tuple.tuple(keyPair.getPrivate(), certificate); + } + + private List defaultSubjectAlternativeNames() { + return List.of( + new GeneralName(GeneralName.registeredID, "1.2.3.4.5.5"), + new GeneralName(GeneralName.dNSName, "localhost"), + new GeneralName(GeneralName.iPAddress, "127.0.0.1") + ); + } + + public X509Certificate toX509Certificate(final X509CertificateHolder x509CertificateHolder) throws CertificateException { + return new JcaX509CertificateConverter().getCertificate(x509CertificateHolder); + } + + private X509v3CertificateBuilder createCertificateBuilder( + final String subject, + final String issuer, + final PublicKey certificatePublicKey, + final PublicKey parentPublicKey, + final BigInteger serialNumber, + final Instant startDate, + final Instant endDate + ) throws NoSuchAlgorithmException, CertIOException { + // CS-SUPPRESS-SINGLE: RegexpSingleline Extension should only be used sparingly to keep implementations as generic as possible + final var subjectName = new X500Name(RFC4519Style.INSTANCE, subject); + final var issuerName = new X500Name(RFC4519Style.INSTANCE, issuer); + final var extUtils = new JcaX509ExtensionUtils(); + return new X509v3CertificateBuilder( + issuerName, + serialNumber, + Date.from(startDate), + Date.from(endDate), + subjectName, + SubjectPublicKeyInfo.getInstance(certificatePublicKey.getEncoded()) + ).addExtension(Extension.authorityKeyIdentifier, false, extUtils.createAuthorityKeyIdentifier(parentPublicKey)) + .addExtension(Extension.subjectKeyIdentifier, false, extUtils.createSubjectKeyIdentifier(certificatePublicKey)); + // CS-ENFORCE-SINGLE + } + + Tuple generateStartAndEndDate() { + final var startDate = Instant.now().minusMillis(24 * 3600 * 1000); + final var endDate = Instant.from(startDate).plus(10, ChronoUnit.DAYS); + return Tuple.tuple(startDate, endDate); + } + + public BigInteger generateSerialNumber() { + return BigInteger.valueOf(Instant.now().plusMillis(100).getEpochSecond()); + } + +} diff --git a/src/test/java/org/opensearch/security/ssl/CertificatesUtils.java b/src/test/java/org/opensearch/security/ssl/CertificatesUtils.java new file mode 100644 index 0000000000..7b6ee9fc74 --- /dev/null +++ b/src/test/java/org/opensearch/security/ssl/CertificatesUtils.java @@ -0,0 +1,43 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.ssl; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.PrivateKey; +import java.security.SecureRandom; + +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.openssl.PKCS8Generator; +import org.bouncycastle.openssl.jcajce.JcaPEMWriter; +import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8EncryptorBuilder; +import org.bouncycastle.util.io.pem.PemObject; + +public class CertificatesUtils { + + public static void writePemContent(final Path path, final Object pemContent) throws IOException { + try (JcaPEMWriter writer = new JcaPEMWriter(Files.newBufferedWriter(path))) { + writer.writeObject(pemContent); + } + } + + public static PemObject privateKeyToPemObject(final PrivateKey privateKey, final String password) throws Exception { + return new PKCS8Generator( + PrivateKeyInfo.getInstance(privateKey.getEncoded()), + new JceOpenSSLPKCS8EncryptorBuilder(PKCS8Generator.PBE_SHA1_3DES).setRandom(new SecureRandom()) + .setPassword(password.toCharArray()) + .build() + ).generate(); + } + +} diff --git a/src/test/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPluginTest.java b/src/test/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPluginTest.java index aefb12c0db..e7e5abaeda 100644 --- a/src/test/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPluginTest.java +++ b/src/test/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPluginTest.java @@ -9,6 +9,7 @@ package org.opensearch.security.ssl; import java.io.IOException; +import java.nio.file.Path; import java.util.Collection; import java.util.List; import java.util.Map; @@ -26,6 +27,7 @@ import org.opensearch.common.network.NetworkModule; import org.opensearch.common.settings.ClusterSettings; import org.opensearch.common.settings.Settings; +import org.opensearch.env.Environment; import org.opensearch.http.HttpServerTransport; import org.opensearch.http.netty4.ssl.SecureNetty4HttpServerTransport; import org.opensearch.plugins.SecureHttpTransportSettingsProvider; @@ -55,17 +57,17 @@ public class OpenSearchSecuritySSLPluginTest extends AbstractSecurityUnitTest { private SecureTransportSettingsProvider secureTransportSettingsProvider; private ClusterSettings clusterSettings; + private Path osPathHome; + @Before public void setUp() { + osPathHome = FileHelper.getAbsoluteFilePathFromClassPath("ssl/kirk-keystore.jks").getParent().getParent(); settings = Settings.builder() + .put(Environment.PATH_HOME_SETTING.getKey(), osPathHome) .put( SSLConfigConstants.SECURITY_SSL_TRANSPORT_KEYSTORE_FILEPATH, FileHelper.getAbsoluteFilePathFromClassPath("ssl/kirk-keystore.jks") ) - .put( - SSLConfigConstants.SECURITY_SSL_HTTP_PEMTRUSTEDCAS_FILEPATH, - FileHelper.getAbsoluteFilePathFromClassPath("ssl/root-ca.pem") - ) .put( SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH, FileHelper.getAbsoluteFilePathFromClassPath("ssl/truststore.jks") @@ -116,7 +118,7 @@ public Optional buildSecureHttpServerEngine(Settings settings, HttpSe @Test public void testRegisterSecureHttpTransport() throws IOException { - try (OpenSearchSecuritySSLPlugin plugin = new OpenSearchSecuritySSLPlugin(settings, null, false)) { + try (OpenSearchSecuritySSLPlugin plugin = new OpenSearchSecuritySSLPlugin(settings, osPathHome, false)) { final Map> transports = plugin.getSecureHttpTransports( settings, MOCK_POOL, @@ -140,7 +142,7 @@ public void testRegisterSecureHttpTransport() throws IOException { @Test public void testRegisterSecureTransport() throws IOException { - try (OpenSearchSecuritySSLPlugin plugin = new OpenSearchSecuritySSLPlugin(settings, null, false)) { + try (OpenSearchSecuritySSLPlugin plugin = new OpenSearchSecuritySSLPlugin(settings, osPathHome, false)) { final Map> transports = plugin.getSecureTransports( settings, MOCK_POOL, @@ -165,7 +167,7 @@ public void testRegisterSecureTransportWithDeprecatedSecuirtyPluginSettings() th .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION, false) .build(); - try (OpenSearchSecuritySSLPlugin plugin = new OpenSearchSecuritySSLPlugin(deprecated, null, false)) { + try (OpenSearchSecuritySSLPlugin plugin = new OpenSearchSecuritySSLPlugin(deprecated, osPathHome, false)) { final Map> transports = plugin.getSecureTransports( deprecated, MOCK_POOL, @@ -190,7 +192,7 @@ public void testRegisterSecureTransportWithNetworkModuleSettings() throws IOExce .put(NetworkModule.TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION_KEY, false) .build(); - try (OpenSearchSecuritySSLPlugin plugin = new OpenSearchSecuritySSLPlugin(migrated, null, false)) { + try (OpenSearchSecuritySSLPlugin plugin = new OpenSearchSecuritySSLPlugin(migrated, osPathHome, false)) { final Map> transports = plugin.getSecureTransports( migrated, MOCK_POOL, @@ -229,7 +231,7 @@ public void testRegisterSecureTransportWithDuplicateSettings() throws IOExceptio .put(NetworkModule.TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION_KEY, false) .build(); - try (OpenSearchSecuritySSLPlugin plugin = new OpenSearchSecuritySSLPlugin(migrated, null, false)) { + try (OpenSearchSecuritySSLPlugin plugin = new OpenSearchSecuritySSLPlugin(migrated, osPathHome, false)) { final Map> transports = plugin.getSecureTransports( migrated, MOCK_POOL, diff --git a/src/test/java/org/opensearch/security/ssl/SSLTest.java b/src/test/java/org/opensearch/security/ssl/SSLTest.java index a6013c7823..20887fccdf 100644 --- a/src/test/java/org/opensearch/security/ssl/SSLTest.java +++ b/src/test/java/org/opensearch/security/ssl/SSLTest.java @@ -569,7 +569,7 @@ public void testHttpsAndNodeSSLFailedCipher() throws Exception { Assert.fail(); } catch (Exception e1) { Throwable e = ExceptionUtils.getRootCause(e1); - Assert.assertTrue(e.toString(), e.toString().contains("no valid cipher")); + Assert.assertTrue(e.toString(), e.toString().contains("No valid cipher")); } } diff --git a/src/test/java/org/opensearch/security/ssl/SecuritySSLReloadCertsActionTests.java b/src/test/java/org/opensearch/security/ssl/SecuritySSLReloadCertsActionTests.java index 244967cf76..30635477eb 100644 --- a/src/test/java/org/opensearch/security/ssl/SecuritySSLReloadCertsActionTests.java +++ b/src/test/java/org/opensearch/security/ssl/SecuritySSLReloadCertsActionTests.java @@ -147,9 +147,12 @@ public void testSSLReloadFail_InvalidDNAndDate() throws Exception { RestHelper.HttpResponse reloadCertsResponse = rh.executePutRequest(RELOAD_TRANSPORT_CERTS_ENDPOINT, null); assertThat(reloadCertsResponse.getStatusCode(), is(500)); assertThat( - "OpenSearchSecurityException[Error while initializing transport SSL layer from PEM: java.lang.Exception: " - + "New Certs do not have valid Issuer DN, Subject DN or SAN.]; nested: Exception[New Certs do not have valid Issuer DN, Subject DN or SAN.];", - is(DefaultObjectMapper.readTree(reloadCertsResponse.getBody()).get("error").get("root_cause").get(0).get("reason").asText()) + DefaultObjectMapper.readTree(reloadCertsResponse.getBody()).get("error").get("root_cause").get(0).get("reason").asText(), + is( + "java.security.cert.CertificateException: " + + "New certificates do not have valid Subject DNs. Current Subject DNs [CN=node-1.example.com,OU=SSL,O=Test,L=Test,C=DE] " + + "new Subject DNs [CN=node-2.example.com,OU=SSL,O=Test,L=Test,C=DE]" + ) ); } diff --git a/src/test/java/org/opensearch/security/ssl/SslContextHandlerTest.java b/src/test/java/org/opensearch/security/ssl/SslContextHandlerTest.java new file mode 100644 index 0000000000..4dea300754 --- /dev/null +++ b/src/test/java/org/opensearch/security/ssl/SslContextHandlerTest.java @@ -0,0 +1,266 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.ssl; + +import java.nio.file.Path; +import java.security.PrivateKey; +import java.security.cert.CertificateException; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.cert.X509CertificateHolder; + +import org.opensearch.common.settings.Settings; +import org.opensearch.security.ssl.config.KeyStoreConfiguration; +import org.opensearch.security.ssl.config.SslParameters; +import org.opensearch.security.ssl.config.TrustStoreConfiguration; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.opensearch.security.ssl.CertificatesUtils.privateKeyToPemObject; +import static org.opensearch.security.ssl.CertificatesUtils.writePemContent; +import static org.junit.Assert.assertThrows; + +public class SslContextHandlerTest { + + @ClassRule + public static CertificatesRule certificatesRule = new CertificatesRule(); + + Path caCertificatePath; + + Path accessCertificatePath; + + Path accessCertificatePrivateKeyPath; + + @Before + public void setUp() throws Exception { + caCertificatePath = certificatesRule.configRootFolder().resolve("ca_certificate.pem"); + accessCertificatePath = certificatesRule.configRootFolder().resolve("access_certificate.pem"); + accessCertificatePrivateKeyPath = certificatesRule.configRootFolder().resolve("access_certificate_pk.pem"); + writeCertificates( + certificatesRule.caCertificateHolder(), + certificatesRule.accessCertificateHolder(), + certificatesRule.accessCertificatePrivateKey() + ); + } + + void writeCertificates( + final X509CertificateHolder caCertificate, + final X509CertificateHolder accessCertificate, + final PrivateKey accessPrivateKey + ) throws Exception { + writePemContent(caCertificatePath, caCertificate); + writePemContent(accessCertificatePath, accessCertificate); + writePemContent(accessCertificatePrivateKeyPath, privateKeyToPemObject(accessPrivateKey, certificatesRule.privateKeyPassword())); + } + + @Test + public void doesNothingIfCertificatesAreSame() throws Exception { + final var sslContextHandler = sslContextHandler(); + + final var sslContextBefore = sslContextHandler.sslContext(); + sslContextHandler.reloadSslContext(); + + assertThat("SSL Context is the same", sslContextBefore.equals(sslContextHandler.sslContext())); + } + + @Test + public void failsIfCertificatesHasInvalidDates() throws Exception { + final var sslContextHandler = sslContextHandler(); + + final var accessCertificate = certificatesRule.x509AccessCertificate(); + final var keyPair = certificatesRule.generateKeyPair(); + final var newCaCertificate = certificatesRule.generateCaCertificate(keyPair); + var newAccessCertificate = certificatesRule.generateAccessCertificate( + keyPair, + accessCertificate.getNotBefore().toInstant(), + accessCertificate.getNotAfter().toInstant().minus(10, ChronoUnit.DAYS) + ); + + writeCertificates(newCaCertificate, newAccessCertificate.v2(), newAccessCertificate.v1()); + + assertThrows(CertificateException.class, sslContextHandler::reloadSslContext); + + newAccessCertificate = certificatesRule.generateAccessCertificate( + keyPair, + accessCertificate.getNotBefore().toInstant().plus(10, ChronoUnit.DAYS), + accessCertificate.getNotAfter().toInstant().plus(20, ChronoUnit.DAYS) + ); + writeCertificates(newCaCertificate, newAccessCertificate.v2(), newAccessCertificate.v1()); + + assertThrows(CertificateException.class, sslContextHandler::reloadSslContext); + } + + @Test + public void filesIfHasNotValidSubjectDNs() throws Exception { + final var sslContextHandler = sslContextHandler(); + + final var keyPair = certificatesRule.generateKeyPair(); + final var newCaCertificate = certificatesRule.generateCaCertificate(keyPair); + final var currentAccessCertificate = certificatesRule.x509AccessCertificate(); + final var wrongSubjectAccessCertificate = certificatesRule.generateAccessCertificate( + keyPair, + "CN=ddddd,O=client,L=test,C=de", + currentAccessCertificate.getIssuerX500Principal().getName() + ); + + writeCertificates(newCaCertificate, wrongSubjectAccessCertificate.v2(), wrongSubjectAccessCertificate.v1()); + + final var e = assertThrows(CertificateException.class, sslContextHandler::reloadSslContext); + assertThat( + e.getMessage(), + is( + "New certificates do not have valid Subject DNs. " + + "Current Subject DNs [CN=some_access,OU=client,O=client,L=test,C=de] " + + "new Subject DNs [CN=ddddd,O=client,L=test,C=de]" + ) + ); + } + + @Test + public void filesIfHasNotValidIssuerDNs() throws Exception { + final var sslContextHandler = sslContextHandler(); + + final var keyPair = certificatesRule.generateKeyPair(); + final var newCaCertificate = certificatesRule.generateCaCertificate(keyPair); + final var currentAccessCertificate = certificatesRule.x509AccessCertificate(); + final var wrongSubjectAccessCertificate = certificatesRule.generateAccessCertificate( + keyPair, + currentAccessCertificate.getSubjectX500Principal().getName(), + "CN=ddddd,O=client,L=test,C=de" + ); + + writeCertificates(newCaCertificate, wrongSubjectAccessCertificate.v2(), wrongSubjectAccessCertificate.v1()); + + final var e = assertThrows(CertificateException.class, sslContextHandler::reloadSslContext); + assertThat( + e.getMessage(), + is( + "New certificates do not have valid Issuer DNs. " + + "Current Issuer DNs: [CN=some_access,OU=client,O=client,L=test,C=de] " + + "new Issuer DNs: [CN=ddddd,O=client,L=test,C=de]" + ) + ); + } + + @Test + public void filesIfHasNotValidSans() throws Exception { + final var sslContextHandler = sslContextHandler(); + + final var keyPair = certificatesRule.generateKeyPair(); + final var newCaCertificate = certificatesRule.generateCaCertificate(keyPair); + final var wrongSubjectAccessCertificate = certificatesRule.generateAccessCertificate( + keyPair, + List.of(new GeneralName(GeneralName.iPAddress, "127.0.0.3")) + ); + + writeCertificates(newCaCertificate, wrongSubjectAccessCertificate.v2(), wrongSubjectAccessCertificate.v1()); + + final var e = assertThrows(CertificateException.class, sslContextHandler::reloadSslContext); + assertThat( + e.getMessage(), + is( + "New certificates do not have valid SANs. " + + "Current SANs: [[[2, localhost], [7, 127.0.0.1], [8, 1.2.3.4.5.5]]] " + + "new SANs: [[[7, 127.0.0.3]]]" + ) + ); + } + + @Test + public void reloadSslContext() throws Exception { + final var sslContextHandler = sslContextHandler(); + + final var sslContextBefore = sslContextHandler.sslContext(); + + final var keyPair = certificatesRule.generateKeyPair(); + final var newCaCertificate = certificatesRule.generateCaCertificate(keyPair); + final var currentAccessCertificate = certificatesRule.x509AccessCertificate(); + final var newAccessCertificate = certificatesRule.generateAccessCertificate( + keyPair, + currentAccessCertificate.getNotBefore().toInstant(), + currentAccessCertificate.getNotAfter().toInstant().plus(10, ChronoUnit.MINUTES) + ); + + writeCertificates(newCaCertificate, newAccessCertificate.v2(), newAccessCertificate.v1()); + + sslContextHandler.reloadSslContext(); + + assertThat("Context reloaded", is(not(sslContextBefore.equals(sslContextHandler.sslContext())))); + } + + @Test + public void reloadSslContextForShuffledSameSans() throws Exception { + final var sslContextHandler = sslContextHandler(); + + final var sslContextBefore = sslContextHandler.sslContext(); + + final var keyPair = certificatesRule.generateKeyPair(); + final var newCaCertificate = certificatesRule.generateCaCertificate(keyPair); + final var currentAccessCertificate = certificatesRule.accessCertificateHolder(); + + // CS-SUPPRESS-SINGLE: RegexpSingleline Extension should only be used sparingly to keep implementations as generic as possible + final var newAccessCertificate = certificatesRule.generateAccessCertificate( + keyPair, + currentAccessCertificate.getNotBefore().toInstant(), + currentAccessCertificate.getNotAfter().toInstant().plus(10, ChronoUnit.MINUTES), + shuffledSans(currentAccessCertificate.getExtension(Extension.subjectAlternativeName)) + ); + // CS-ENFORCE-SINGLE + + writeCertificates(newCaCertificate, newAccessCertificate.v2(), newAccessCertificate.v1()); + + sslContextHandler.reloadSslContext(); + + assertThat("Context reloaded", is(not(sslContextBefore.equals(sslContextHandler.sslContext())))); + } + + // CS-SUPPRESS-SINGLE: RegexpSingleline Extension should only be used sparingly to keep implementations as generic as possible + List shuffledSans(Extension currentSans) { + final var san1Sequence = ASN1Sequence.getInstance(currentSans.getParsedValue().toASN1Primitive()); + + final var shuffledSans = new ArrayList(); + final var objects = san1Sequence.getObjects(); + while (objects.hasMoreElements()) { + shuffledSans.add(GeneralName.getInstance(objects.nextElement())); + } + + for (int i = 0; i < 5; i++) + Collections.shuffle(shuffledSans); + return shuffledSans; + } + // CS-ENFORCE-SINGLE + + SslContextHandler sslContextHandler() { + final var sslParameters = SslParameters.loader(Settings.EMPTY).load(false); + final var trustStoreConfiguration = new TrustStoreConfiguration.PemTrustStoreConfiguration(caCertificatePath); + final var keyStoreConfiguration = new KeyStoreConfiguration.PemKeyStoreConfiguration( + accessCertificatePath, + accessCertificatePrivateKeyPath, + certificatesRule.privateKeyPassword().toCharArray() + ); + + SslConfiguration sslConfiguration = new SslConfiguration(sslParameters, trustStoreConfiguration, keyStoreConfiguration); + return new SslContextHandler(sslConfiguration, false); + } + +} diff --git a/src/test/java/org/opensearch/security/ssl/SslSettingsManagerTest.java b/src/test/java/org/opensearch/security/ssl/SslSettingsManagerTest.java new file mode 100644 index 0000000000..1aa2c47eb3 --- /dev/null +++ b/src/test/java/org/opensearch/security/ssl/SslSettingsManagerTest.java @@ -0,0 +1,464 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.ssl; + +import java.nio.file.Path; +import java.util.List; +import java.util.Locale; + +import com.carrotsearch.randomizedtesting.RandomizedTest; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; + +import org.opensearch.OpenSearchException; +import org.opensearch.common.settings.MockSecureSettings; +import org.opensearch.common.settings.Settings; +import org.opensearch.env.Environment; +import org.opensearch.env.TestEnvironment; +import org.opensearch.security.ssl.config.CertType; + +import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.SslContext; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.opensearch.security.ssl.CertificatesUtils.privateKeyToPemObject; +import static org.opensearch.security.ssl.CertificatesUtils.writePemContent; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_CLIENTAUTH_MODE; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_KEYSTORE_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_PEMCERT_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_PEMKEY_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_PEMTRUSTEDCAS_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_CLIENT_PEMCERT_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_CLIENT_PEMKEY_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_CLIENT_PEMTRUSTEDCAS_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENABLED; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_KEYSTORE_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_PEMCERT_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_PEMKEY_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_PEMTRUSTEDCAS_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_SERVER_PEMCERT_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_SERVER_PEMKEY_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_SERVER_PEMTRUSTEDCAS_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_HTTP_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_TRANSPORT_CLIENT_EXTENDED_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_TRANSPORT_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_TRANSPORT_SERVER_EXTENDED_PREFIX; +import static org.opensearch.security.support.ConfigConstants.SECURITY_SSL_ONLY; +import static org.junit.Assert.assertThrows; + +public class SslSettingsManagerTest extends RandomizedTest { + + @ClassRule + public static CertificatesRule certificatesRule = new CertificatesRule(); + + @BeforeClass + public static void setUp() throws Exception { + writeCertificates("ca_http_certificate.pem", "access_http_certificate.pem", "access_http_certificate_pk.pem"); + writeCertificates("ca_transport_certificate.pem", "access_transport_certificate.pem", "access_transport_certificate_pk.pem"); + } + + static void writeCertificates(final String trustedFileName, final String accessFileName, final String accessPkFileName) + throws Exception { + writePemContent(path(trustedFileName), certificatesRule.caCertificateHolder()); + writePemContent(path(accessFileName), certificatesRule.accessCertificateHolder()); + writePemContent( + path(accessPkFileName), + privateKeyToPemObject(certificatesRule.accessCertificatePrivateKey(), certificatesRule.privateKeyPassword()) + ); + } + + static Path path(final String fileName) { + return certificatesRule.configRootFolder().resolve(fileName); + } + + @Test + public void failsIfNoSslSet() throws Exception { + final var settings = defaultSettingsBuilder().build(); + assertThrows(OpenSearchException.class, () -> new SslSettingsManager(TestEnvironment.newEnvironment(settings))); + } + + @Test + public void transportFailsIfNoConfigDefine() throws Exception { + final var noTransportSettings = defaultSettingsBuilder().put(SECURITY_SSL_HTTP_ENABLED, true).build(); + assertThrows(OpenSearchException.class, () -> new SslSettingsManager(TestEnvironment.newEnvironment(noTransportSettings))); + } + + @Test + public void transportFailsIfConfigEnabledButNotDefined() throws Exception { + final var noTransportSettingsButItEnabled = defaultSettingsBuilder().put(SECURITY_SSL_TRANSPORT_ENABLED, true).build(); + assertThrows( + OpenSearchException.class, + () -> new SslSettingsManager(TestEnvironment.newEnvironment(noTransportSettingsButItEnabled)) + ); + } + + @Test + public void transportFailsIfJdkTrustStoreHasNotBeenSet() throws Exception { + final var noTransportSettingsButItEnabled = defaultSettingsBuilder().put(SECURITY_SSL_TRANSPORT_ENABLED, true) + .put(SECURITY_SSL_TRANSPORT_KEYSTORE_FILEPATH, certificatesRule.configRootFolder().toString()) + .build(); + assertThrows( + OpenSearchException.class, + () -> new SslSettingsManager(TestEnvironment.newEnvironment(noTransportSettingsButItEnabled)) + ); + } + + @Test + public void transportFailsIfExtendedKeyUsageEnabledForJdkKeyStoreButNotConfigured() throws Exception { + final var noTransportSettingsButItEnabled = defaultSettingsBuilder().put(SECURITY_SSL_TRANSPORT_ENABLED, true) + .put(SECURITY_SSL_TRANSPORT_KEYSTORE_FILEPATH, certificatesRule.configRootFolder().toString()) + .put(SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH, certificatesRule.configRootFolder().toString()) + .put(SECURITY_SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED, true) + .build(); + assertThrows( + OpenSearchException.class, + () -> new SslSettingsManager(TestEnvironment.newEnvironment(noTransportSettingsButItEnabled)) + ); + } + + @Test + public void transportFailsIfExtendedKeyUsageEnabledForPemKeyStoreButNotConfigured() throws Exception { + final var noTransportSettingsButItEnabled = defaultSettingsBuilder().put(SECURITY_SSL_TRANSPORT_ENABLED, true) + .put(SECURITY_SSL_TRANSPORT_PEMCERT_FILEPATH, certificatesRule.configRootFolder().toString()) + .put(SECURITY_SSL_TRANSPORT_PEMKEY_FILEPATH, certificatesRule.configRootFolder().toString()) + .put(SECURITY_SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED, true) + .build(); + assertThrows( + OpenSearchException.class, + () -> new SslSettingsManager(TestEnvironment.newEnvironment(noTransportSettingsButItEnabled)) + ); + } + + @Test + public void transportFailsIfConfigDisabled() throws Exception { + Settings settings = defaultSettingsBuilder().put(SECURITY_SSL_HTTP_ENABLED, true) + .put(SECURITY_SSL_TRANSPORT_ENABLED, false) + .build(); + assertThrows(OpenSearchException.class, () -> new SslSettingsManager(TestEnvironment.newEnvironment(settings))); + } + + @Test + public void httpConfigFailsIfBothPemAndJDKSettingsWereSet() throws Exception { + final var keyStoreSettings = randomFrom(List.of(SECURITY_SSL_HTTP_KEYSTORE_FILEPATH)); + final var pemKeyStoreSettings = randomFrom( + List.of(SECURITY_SSL_HTTP_PEMKEY_FILEPATH, SECURITY_SSL_HTTP_PEMCERT_FILEPATH, SECURITY_SSL_HTTP_PEMTRUSTEDCAS_FILEPATH) + ); + final var settings = defaultSettingsBuilder().put(SECURITY_SSL_HTTP_ENABLED, true) + .put(keyStoreSettings, "aaa") + .put(pemKeyStoreSettings, "bbb") + .build(); + assertThrows(OpenSearchException.class, () -> new SslSettingsManager(TestEnvironment.newEnvironment(settings))); + } + + @Test + public void httpConfigFailsIfHttpEnabledButButNotDefined() throws Exception { + final var settings = defaultSettingsBuilder().put(SECURITY_SSL_HTTP_ENABLED, true).build(); + assertThrows(OpenSearchException.class, () -> new SslSettingsManager(TestEnvironment.newEnvironment(settings))); + } + + @Test + public void httpConfigFailsIfClientAuthRequiredAndJdkTrustStoreNotSet() throws Exception { + final var settings = defaultSettingsBuilder().put(SECURITY_SSL_HTTP_ENABLED, true) + .put(SECURITY_SSL_HTTP_CLIENTAUTH_MODE, ClientAuth.REQUIRE.name().toLowerCase(Locale.ROOT)) + .put(SECURITY_SSL_HTTP_KEYSTORE_FILEPATH, certificatesRule.configRootFolder().toString()) + .build(); + assertThrows(OpenSearchException.class, () -> new SslSettingsManager(TestEnvironment.newEnvironment(settings))); + } + + @Test + public void httpConfigFailsIfClientAuthRequiredAndPemTrustedCasNotSet() throws Exception { + final var settings = defaultSettingsBuilder().put(SECURITY_SSL_HTTP_ENABLED, true) + .put(SECURITY_SSL_HTTP_CLIENTAUTH_MODE, ClientAuth.REQUIRE.name().toLowerCase(Locale.ROOT)) + .put(SECURITY_SSL_HTTP_PEMKEY_FILEPATH, "aaa") + .put(SECURITY_SSL_HTTP_PEMCERT_FILEPATH, "bbb") + .build(); + assertThrows(OpenSearchException.class, () -> new SslSettingsManager(TestEnvironment.newEnvironment(settings))); + } + + @Test + public void loadConfigurationAndBuildHSslContextForSslOnlyMode() throws Exception { + final var securitySettings = new MockSecureSettings(); + securitySettings.setString(SSL_TRANSPORT_PREFIX + "pemkey_password_secure", certificatesRule.privateKeyPassword()); + securitySettings.setString(SSL_HTTP_PREFIX + "pemkey_password_secure", certificatesRule.privateKeyPassword()); + final var settingsBuilder = defaultSettingsBuilder().setSecureSettings(securitySettings); + withTransportSslSettings( + settingsBuilder, + "ca_transport_certificate.pem", + "access_transport_certificate.pem", + "access_transport_certificate_pk.pem" + ); + withHttpSslSettings(settingsBuilder); + final var transportEnabled = randomBoolean(); + final var sslSettingsManager = new SslSettingsManager( + TestEnvironment.newEnvironment( + settingsBuilder.put(SECURITY_SSL_TRANSPORT_ENABLED, transportEnabled).put(SECURITY_SSL_ONLY, true).build() + ) + ); + + assertThat("Loaded HTTP configuration", sslSettingsManager.sslConfiguration(CertType.HTTP).isPresent()); + if (transportEnabled) { + assertThat("Loaded Transport configuration", sslSettingsManager.sslConfiguration(CertType.TRANSPORT).isPresent()); + assertThat("Loaded Transport Client configuration", sslSettingsManager.sslConfiguration(CertType.TRANSPORT_CLIENT).isPresent()); + } else { + assertThat("Didn't load Transport configuration", sslSettingsManager.sslConfiguration(CertType.TRANSPORT).isEmpty()); + assertThat( + "Didn't load Transport Client configuration", + sslSettingsManager.sslConfiguration(CertType.TRANSPORT_CLIENT).isEmpty() + ); + } + + assertThat("Built HTTP SSL Context", sslSettingsManager.sslContextHandler(CertType.HTTP).isPresent()); + if (transportEnabled) { + assertThat("Built Transport SSL Context", sslSettingsManager.sslContextHandler(CertType.TRANSPORT).isPresent()); + assertThat("Built Client SSL Context", sslSettingsManager.sslContextHandler(CertType.TRANSPORT_CLIENT).isPresent()); + } else { + assertThat("Didn't build Transport SSL Context", sslSettingsManager.sslContextHandler(CertType.TRANSPORT).isEmpty()); + assertThat("Didn't build Client SSL Context", sslSettingsManager.sslContextHandler(CertType.TRANSPORT_CLIENT).isEmpty()); + } + + assertThat( + "Built Server SSL context for HTTP", + sslSettingsManager.sslContextHandler(CertType.HTTP).map(SslContextHandler::sslContext).map(SslContext::isServer).orElse(false) + ); + } + + @Test + public void loadConfigurationAndBuildSslContextForClientNode() throws Exception { + final var securitySettings = new MockSecureSettings(); + securitySettings.setString(SSL_TRANSPORT_PREFIX + "pemkey_password_secure", certificatesRule.privateKeyPassword()); + securitySettings.setString(SSL_HTTP_PREFIX + "pemkey_password_secure", certificatesRule.privateKeyPassword()); + final var settingsBuilder = defaultSettingsBuilder().setSecureSettings(securitySettings); + withTransportSslSettings( + settingsBuilder, + "ca_transport_certificate.pem", + "access_transport_certificate.pem", + "access_transport_certificate_pk.pem" + ); + withHttpSslSettings(settingsBuilder); + final var sslSettingsManager = new SslSettingsManager( + TestEnvironment.newEnvironment( + settingsBuilder.put("client.type", "client").put(SECURITY_SSL_HTTP_ENABLED, randomBoolean()).build() + ) + ); + + assertThat("Didn't load HTTP configuration", sslSettingsManager.sslConfiguration(CertType.HTTP).isEmpty()); + assertThat("Loaded Transport configuration", sslSettingsManager.sslConfiguration(CertType.TRANSPORT).isPresent()); + assertThat("Loaded Transport Client configuration", sslSettingsManager.sslConfiguration(CertType.TRANSPORT_CLIENT).isPresent()); + + assertThat("Didn't build HTTP SSL Context", sslSettingsManager.sslContextHandler(CertType.HTTP).isEmpty()); + assertThat("Built Transport SSL Context", sslSettingsManager.sslContextHandler(CertType.TRANSPORT).isPresent()); + assertThat("Built Client SSL Context", sslSettingsManager.sslContextHandler(CertType.TRANSPORT_CLIENT).isPresent()); + + assertThat( + "Built Server SSL context for Transport", + sslSettingsManager.sslContextHandler(CertType.TRANSPORT) + .map(SslContextHandler::sslContext) + .map(SslContext::isServer) + .orElse(false) + ); + assertThat( + "Built Client SSL context for Transport Client", + sslSettingsManager.sslContextHandler(CertType.TRANSPORT_CLIENT) + .map(SslContextHandler::sslContext) + .map(SslContext::isClient) + .orElse(false) + + ); + } + + @Test + public void loadConfigurationAndBuildSslContexts() throws Exception { + final var securitySettings = new MockSecureSettings(); + securitySettings.setString(SSL_TRANSPORT_PREFIX + "pemkey_password_secure", certificatesRule.privateKeyPassword()); + securitySettings.setString(SSL_HTTP_PREFIX + "pemkey_password_secure", certificatesRule.privateKeyPassword()); + final var settingsBuilder = defaultSettingsBuilder().setSecureSettings(securitySettings); + withTransportSslSettings( + settingsBuilder, + "ca_transport_certificate.pem", + "access_transport_certificate.pem", + "access_transport_certificate_pk.pem" + ); + withHttpSslSettings(settingsBuilder); + final var sslSettingsManager = new SslSettingsManager(TestEnvironment.newEnvironment(settingsBuilder.build())); + assertThat("Loaded HTTP configuration", sslSettingsManager.sslConfiguration(CertType.HTTP).isPresent()); + assertThat("Loaded Transport configuration", sslSettingsManager.sslConfiguration(CertType.TRANSPORT).isPresent()); + assertThat("Loaded Transport Client configuration", sslSettingsManager.sslConfiguration(CertType.TRANSPORT_CLIENT).isPresent()); + + assertThat("Built HTTP SSL Context", sslSettingsManager.sslContextHandler(CertType.HTTP).isPresent()); + assertThat("Built Transport SSL Context", sslSettingsManager.sslContextHandler(CertType.TRANSPORT).isPresent()); + assertThat("Built Transport Client SSL Context", sslSettingsManager.sslContextHandler(CertType.TRANSPORT_CLIENT).isPresent()); + + assertThat( + "Built Server SSL context for HTTP", + sslSettingsManager.sslContextHandler(CertType.HTTP).map(SslContextHandler::sslContext).map(SslContext::isServer).orElse(false) + ); + assertThat( + "Built Server SSL context for Transport", + sslSettingsManager.sslContextHandler(CertType.TRANSPORT) + .map(SslContextHandler::sslContext) + .map(SslContext::isServer) + .orElse(false) + ); + assertThat( + "Built Client SSL context for Transport Client", + sslSettingsManager.sslContextHandler(CertType.TRANSPORT_CLIENT) + .map(SslContextHandler::sslContext) + .map(SslContext::isClient) + .orElse(false) + + ); + } + + @Test + public void loadConfigurationAndBuildTransportSslContext() throws Exception { + final var securitySettings = new MockSecureSettings(); + securitySettings.setString(SSL_TRANSPORT_PREFIX + "pemkey_password_secure", certificatesRule.privateKeyPassword()); + final var settingsBuilder = defaultSettingsBuilder().setSecureSettings(securitySettings); + withTransportSslSettings( + settingsBuilder, + "ca_transport_certificate.pem", + "access_transport_certificate.pem", + "access_transport_certificate_pk.pem" + ); + final var sslSettingsManager = new SslSettingsManager(TestEnvironment.newEnvironment(settingsBuilder.build())); + + assertThat("Didn't load HTTP configuration", sslSettingsManager.sslConfiguration(CertType.HTTP).isEmpty()); + assertThat("Loaded Transport configuration", sslSettingsManager.sslConfiguration(CertType.TRANSPORT).isPresent()); + assertThat("Loaded Transport Client configuration", sslSettingsManager.sslConfiguration(CertType.TRANSPORT_CLIENT).isPresent()); + assertThat( + "SSL configuration for Transport and Transport Client is the same", + sslSettingsManager.sslConfiguration(CertType.TRANSPORT) + .flatMap(t -> sslSettingsManager.sslConfiguration(CertType.TRANSPORT_CLIENT).map(tc -> tc.equals(t))) + .orElse(false) + ); + + assertThat("Built HTTP SSL Context", sslSettingsManager.sslContextHandler(CertType.HTTP).isEmpty()); + assertThat("Built Transport SSL Context", sslSettingsManager.sslContextHandler(CertType.TRANSPORT).isPresent()); + assertThat("Built Transport Client SSL Context", sslSettingsManager.sslContextHandler(CertType.TRANSPORT_CLIENT).isPresent()); + + assertThat( + "Built Server SSL context for Transport", + sslSettingsManager.sslContextHandler(CertType.TRANSPORT) + .map(SslContextHandler::sslContext) + .map(SslContext::isServer) + .orElse(false) + + ); + assertThat( + "Built Client SSL context for Transport Client", + sslSettingsManager.sslContextHandler(CertType.TRANSPORT_CLIENT) + .map(SslContextHandler::sslContext) + .map(SslContext::isClient) + .orElse(false) + + ); + } + + @Test + public void loadConfigurationAndBuildExtendedTransportSslContexts() throws Exception { + writeCertificates( + "ca_server_transport_certificate.pem", + "access_server_transport_certificate.pem", + "access_server_transport_certificate_pk.pem" + ); + writeCertificates( + "ca_client_transport_certificate.pem", + "access_client_transport_certificate.pem", + "access_client_transport_certificate_pk.pem" + ); + + final var securitySettings = new MockSecureSettings(); + securitySettings.setString( + SSL_TRANSPORT_PREFIX + SSL_TRANSPORT_SERVER_EXTENDED_PREFIX + "pemkey_password_secure", + certificatesRule.privateKeyPassword() + ); + securitySettings.setString( + SSL_TRANSPORT_PREFIX + SSL_TRANSPORT_CLIENT_EXTENDED_PREFIX + "pemkey_password_secure", + certificatesRule.privateKeyPassword() + ); + final var sslSettingsManager = new SslSettingsManager( + TestEnvironment.newEnvironment( + defaultSettingsBuilder().put(SECURITY_SSL_TRANSPORT_ENABLED, true) + .put(SECURITY_SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED, true) + .put(SECURITY_SSL_TRANSPORT_SERVER_PEMTRUSTEDCAS_FILEPATH, path("ca_server_transport_certificate.pem")) + .put(SECURITY_SSL_TRANSPORT_SERVER_PEMCERT_FILEPATH, path("access_server_transport_certificate.pem")) + .put(SECURITY_SSL_TRANSPORT_SERVER_PEMKEY_FILEPATH, path("access_server_transport_certificate_pk.pem")) + .put(SECURITY_SSL_TRANSPORT_CLIENT_PEMTRUSTEDCAS_FILEPATH, path("ca_client_transport_certificate.pem")) + .put(SECURITY_SSL_TRANSPORT_CLIENT_PEMCERT_FILEPATH, path("access_client_transport_certificate.pem")) + .put(SECURITY_SSL_TRANSPORT_CLIENT_PEMKEY_FILEPATH, path("access_client_transport_certificate_pk.pem")) + .setSecureSettings(securitySettings) + .build() + ) + ); + + assertThat("Didn't load HTTP configuration", sslSettingsManager.sslConfiguration(CertType.HTTP).isEmpty()); + assertThat("Loaded Transport configuration", sslSettingsManager.sslConfiguration(CertType.TRANSPORT).isPresent()); + assertThat("Loaded Transport Client configuration", sslSettingsManager.sslConfiguration(CertType.TRANSPORT_CLIENT).isPresent()); + assertThat( + "SSL configuration for Transport and Transport Client is not the same", + sslSettingsManager.sslConfiguration(CertType.TRANSPORT) + .flatMap(t -> sslSettingsManager.sslConfiguration(CertType.TRANSPORT_CLIENT).map(tc -> !tc.equals(t))) + .orElse(true) + ); + assertThat("Built HTTP SSL Context", sslSettingsManager.sslContextHandler(CertType.HTTP).isEmpty()); + assertThat("Built Transport SSL Context", sslSettingsManager.sslContextHandler(CertType.TRANSPORT).isPresent()); + assertThat("Built Transport Client SSL Context", sslSettingsManager.sslContextHandler(CertType.TRANSPORT_CLIENT).isPresent()); + + assertThat( + "Built Server SSL context for Transport", + sslSettingsManager.sslContextHandler(CertType.TRANSPORT) + .map(SslContextHandler::sslContext) + .map(SslContext::isServer) + .orElse(false) + + ); + assertThat( + "Built Client SSL context for Transport Client", + sslSettingsManager.sslContextHandler(CertType.TRANSPORT_CLIENT) + .map(SslContextHandler::sslContext) + .map(SslContext::isClient) + .orElse(false) + + ); + } + + private void withTransportSslSettings( + final Settings.Builder settingsBuilder, + final String caFileName, + final String accessFileName, + final String accessPkFileName + ) { + settingsBuilder.put(SECURITY_SSL_TRANSPORT_ENABLED, true) + .put(SECURITY_SSL_TRANSPORT_PEMTRUSTEDCAS_FILEPATH, path(caFileName)) + .put(SECURITY_SSL_TRANSPORT_PEMCERT_FILEPATH, path(accessFileName)) + .put(SECURITY_SSL_TRANSPORT_PEMKEY_FILEPATH, path(accessPkFileName)); + } + + private void withHttpSslSettings(final Settings.Builder settingsBuilder) { + settingsBuilder.put(SECURITY_SSL_TRANSPORT_ENABLED, true) + .put(SECURITY_SSL_HTTP_ENABLED, true) + .put(SECURITY_SSL_HTTP_PEMTRUSTEDCAS_FILEPATH, path("ca_http_certificate.pem")) + .put(SECURITY_SSL_HTTP_PEMCERT_FILEPATH, path("access_http_certificate.pem")) + .put(SECURITY_SSL_HTTP_PEMKEY_FILEPATH, path("access_http_certificate_pk.pem")); + } + + Settings.Builder defaultSettingsBuilder() { + return Settings.builder() + .put(Environment.PATH_HOME_SETTING.getKey(), certificatesRule.configRootFolder().toString()) + .put("client.type", "node"); + } + +} diff --git a/src/test/java/org/opensearch/security/ssl/config/CertificateTest.java b/src/test/java/org/opensearch/security/ssl/config/CertificateTest.java new file mode 100644 index 0000000000..5fe2185d44 --- /dev/null +++ b/src/test/java/org/opensearch/security/ssl/config/CertificateTest.java @@ -0,0 +1,38 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.ssl.config; + +import java.lang.reflect.Method; + +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.fail; + +public class CertificateTest { + + @Test + public void testGetObjectMethod() { + try { + final Method method = Certificate.getObjectMethod(); + assertThat("Method should not be null", method, notNullValue()); + assertThat( + "One of the expected methods should be available", + method.getName().equals("getBaseObject") || method.getName().equals("getObject") + ); + } catch (ClassNotFoundException | NoSuchMethodException e) { + fail("Exception should not be thrown: " + e.getMessage()); + } + } + +} diff --git a/src/test/java/org/opensearch/security/ssl/config/JdkSslCertificatesLoaderTest.java b/src/test/java/org/opensearch/security/ssl/config/JdkSslCertificatesLoaderTest.java new file mode 100644 index 0000000000..174f6c0fd5 --- /dev/null +++ b/src/test/java/org/opensearch/security/ssl/config/JdkSslCertificatesLoaderTest.java @@ -0,0 +1,318 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.ssl.config; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import org.junit.Test; + +import org.opensearch.common.collect.Tuple; +import org.opensearch.common.settings.MockSecureSettings; +import org.opensearch.env.TestEnvironment; + +import static java.util.Objects.isNull; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.opensearch.security.ssl.util.SSLConfigConstants.DEFAULT_STORE_PASSWORD; +import static org.opensearch.security.ssl.util.SSLConfigConstants.ENABLED; +import static org.opensearch.security.ssl.util.SSLConfigConstants.KEYSTORE_ALIAS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.KEYSTORE_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.KEYSTORE_TYPE; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_CLIENT_KEYSTORE_ALIAS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_CLIENT_TRUSTSTORE_ALIAS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENABLED; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_KEYSTORE_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_KEYSTORE_TYPE; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_SERVER_KEYSTORE_ALIAS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_SERVER_TRUSTSTORE_ALIAS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_TYPE; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_HTTP_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_TRANSPORT_CLIENT_EXTENDED_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_TRANSPORT_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_TRANSPORT_SERVER_EXTENDED_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.TRUSTSTORE_ALIAS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.TRUSTSTORE_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.TRUSTSTORE_TYPE; + +public class JdkSslCertificatesLoaderTest extends SslCertificatesLoaderTest { + + static final Function resolveKeyStoreType = s -> isNull(s) ? KeyStore.getDefaultType() : s; + + static final String SERVER_TRUSTSTORE_ALIAS = "server-truststore-alias"; + + static final String SERVER_KEYSTORE_ALIAS = "server-keystore-alias"; + + static final String CLIENT_TRUSTSTORE_ALIAS = "client-truststore-alias"; + + static final String CLIENT_KEYSTORE_ALIAS = "client-keystore-alias"; + + @Test + public void loadHttpSslConfigurationFromKeyAndTrustStoreFiles() throws Exception { + testJdkBasedSslConfiguration(SSL_HTTP_PREFIX, randomBoolean()); + } + + @Test + public void loadTransportJdkBasedSslConfiguration() throws Exception { + testJdkBasedSslConfiguration(SSL_TRANSPORT_PREFIX, true); + } + + @Test + public void loadTransportJdkBasedSslExtendedConfiguration() throws Exception { + final var clientKeyPair = certificatesRule.generateKeyPair(); + + final var serverCaCertificate = certificatesRule.x509CaCertificate(); + final var clientCaCertificate = certificatesRule.toX509Certificate(certificatesRule.generateCaCertificate(clientKeyPair)); + + final var serverAccessCertificateKey = certificatesRule.accessCertificatePrivateKey(); + final var serverAccessCertificate = certificatesRule.x509AccessCertificate(); + + final var clientAccessCertificateAndKey = certificatesRule.generateAccessCertificate(clientKeyPair); + + final var clientAccessCertificateKey = clientAccessCertificateAndKey.v1(); + final var clientAccessCertificate = certificatesRule.toX509Certificate(clientAccessCertificateAndKey.v2()); + + final var trustStoreType = randomKeyStoreType(); + final var keyStoreType = randomKeyStoreType(); + + final var useSecurePassword = randomBoolean(); + final var trustStorePassword = randomKeyStorePassword(useSecurePassword); + final var keyStorePassword = randomKeyStorePassword(useSecurePassword); + + final var trustStorePath = createTrustStore( + trustStoreType, + trustStorePassword, + Map.of(SERVER_TRUSTSTORE_ALIAS, serverCaCertificate, CLIENT_TRUSTSTORE_ALIAS, clientCaCertificate) + ); + final var keyStorePath = createKeyStore( + keyStoreType, + keyStorePassword, + Map.of( + SERVER_KEYSTORE_ALIAS, + Tuple.tuple(serverAccessCertificateKey, serverAccessCertificate), + CLIENT_KEYSTORE_ALIAS, + Tuple.tuple(clientAccessCertificateKey, clientAccessCertificate) + ) + ); + + final var settingsBuilder = defaultSettingsBuilder().put(SECURITY_SSL_TRANSPORT_ENABLED, true) + .put(SECURITY_SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED, true) + .put(SECURITY_SSL_TRANSPORT_TRUSTSTORE_TYPE, trustStoreType) + .put(SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH, trustStorePath) + .put(SECURITY_SSL_TRANSPORT_SERVER_TRUSTSTORE_ALIAS, SERVER_TRUSTSTORE_ALIAS) + .put(SECURITY_SSL_TRANSPORT_CLIENT_TRUSTSTORE_ALIAS, CLIENT_TRUSTSTORE_ALIAS) + .put(SECURITY_SSL_TRANSPORT_KEYSTORE_TYPE, keyStoreType) + .put(SECURITY_SSL_TRANSPORT_KEYSTORE_FILEPATH, keyStorePath) + .put(SECURITY_SSL_TRANSPORT_SERVER_KEYSTORE_ALIAS, SERVER_KEYSTORE_ALIAS) + .put(SECURITY_SSL_TRANSPORT_CLIENT_KEYSTORE_ALIAS, CLIENT_KEYSTORE_ALIAS); + + if (useSecurePassword) { + final var securitySettings = new MockSecureSettings(); + securitySettings.setString(SSL_TRANSPORT_PREFIX + "keystore_password_secure", keyStorePassword); + securitySettings.setString(SSL_TRANSPORT_PREFIX + "truststore_password_secure", trustStorePassword); + + securitySettings.setString( + SSL_TRANSPORT_PREFIX + SSL_TRANSPORT_SERVER_EXTENDED_PREFIX + "keystore_keypassword_secure", + certificatesRule.privateKeyPassword() + ); + securitySettings.setString( + SSL_TRANSPORT_PREFIX + SSL_TRANSPORT_CLIENT_EXTENDED_PREFIX + "keystore_keypassword_secure", + certificatesRule.privateKeyPassword() + ); + settingsBuilder.setSecureSettings(securitySettings); + } else { + settingsBuilder.put(SSL_TRANSPORT_PREFIX + "keystore_password", keyStorePassword); + settingsBuilder.put(SSL_TRANSPORT_PREFIX + "truststore_password", trustStorePassword); + + settingsBuilder.put( + SSL_TRANSPORT_PREFIX + SSL_TRANSPORT_SERVER_EXTENDED_PREFIX + "keystore_keypassword", + certificatesRule.privateKeyPassword() + ); + settingsBuilder.put( + SSL_TRANSPORT_PREFIX + SSL_TRANSPORT_CLIENT_EXTENDED_PREFIX + "keystore_keypassword", + certificatesRule.privateKeyPassword() + ); + } + final var settings = settingsBuilder.build(); + + final var serverConfiguration = new SslCertificatesLoader(SSL_TRANSPORT_PREFIX, SSL_TRANSPORT_SERVER_EXTENDED_PREFIX) + .loadConfiguration(TestEnvironment.newEnvironment(settings)); + assertTrustStoreConfiguration( + serverConfiguration.v1(), + trustStorePath, + new Certificate(serverCaCertificate, resolveKeyStoreType.apply(trustStoreType), SERVER_TRUSTSTORE_ALIAS, false) + ); + assertKeyStoreConfiguration( + serverConfiguration.v2(), + List.of(keyStorePath), + new Certificate(serverAccessCertificate, resolveKeyStoreType.apply(keyStoreType), SERVER_KEYSTORE_ALIAS, true) + ); + + final var clientConfiguration = new SslCertificatesLoader(SSL_TRANSPORT_PREFIX, SSL_TRANSPORT_CLIENT_EXTENDED_PREFIX) + .loadConfiguration(TestEnvironment.newEnvironment(settings)); + assertTrustStoreConfiguration( + clientConfiguration.v1(), + trustStorePath, + new Certificate(clientCaCertificate, resolveKeyStoreType.apply(trustStoreType), CLIENT_TRUSTSTORE_ALIAS, false) + ); + assertKeyStoreConfiguration( + clientConfiguration.v2(), + List.of(keyStorePath), + new Certificate(clientAccessCertificate, resolveKeyStoreType.apply(keyStoreType), CLIENT_KEYSTORE_ALIAS, true) + ); + } + + private void testJdkBasedSslConfiguration(final String sslConfigPrefix, final boolean useAuthorityCertificate) throws Exception { + final var useSecurePassword = randomBoolean(); + + final var keyPair = certificatesRule.generateKeyPair(); + final var trustStoreCertificates = Map.of( + "default-truststore-alias", + certificatesRule.x509CaCertificate(), + "another-truststore-alias", + certificatesRule.toX509Certificate(certificatesRule.generateCaCertificate(keyPair)) + ); + + final var keysAndCertificate = certificatesRule.generateAccessCertificate(keyPair); + final var keyStoreCertificates = Map.of( + "default-keystore-alias", + Tuple.tuple(certificatesRule.accessCertificatePrivateKey(), certificatesRule.x509AccessCertificate()), + "another-keystore-alias", + Tuple.tuple(keysAndCertificate.v1(), certificatesRule.toX509Certificate(keysAndCertificate.v2())) + ); + + final var trustStoreAlias = randomFrom(new String[] { "default-truststore-alias", "another-truststore-alias", null }); + final var keyStoreAlias = (String) null;// randomFrom(new String[] { "default-keystore-alias", "another-keystore-alias", null }); + + final var keyStorePassword = randomKeyStorePassword(useSecurePassword); + final var trustStorePassword = randomKeyStorePassword(useSecurePassword); + + final var keyStoreType = randomKeyStoreType(); + final var keyStorePath = createKeyStore(keyStoreType, keyStorePassword, keyStoreCertificates); + + final var trustStoreType = randomKeyStoreType(); + final var trustStorePath = createTrustStore(trustStoreType, trustStorePassword, trustStoreCertificates); + + final var settingsBuilder = defaultSettingsBuilder().put(sslConfigPrefix + ENABLED, true) + .put(sslConfigPrefix + KEYSTORE_FILEPATH, keyStorePath) + .put(sslConfigPrefix + KEYSTORE_ALIAS, keyStoreAlias) + .put(sslConfigPrefix + KEYSTORE_TYPE, keyStoreType); + if (useAuthorityCertificate) { + settingsBuilder.put(sslConfigPrefix + TRUSTSTORE_FILEPATH, trustStorePath) + .put(sslConfigPrefix + TRUSTSTORE_ALIAS, trustStoreAlias) + .put(sslConfigPrefix + TRUSTSTORE_TYPE, trustStoreType); + } + if (useSecurePassword) { + final var securitySettings = new MockSecureSettings(); + securitySettings.setString(sslConfigPrefix + "keystore_password_secure", keyStorePassword); + securitySettings.setString(sslConfigPrefix + "keystore_keypassword_secure", certificatesRule.privateKeyPassword()); + if (useAuthorityCertificate) { + securitySettings.setString(sslConfigPrefix + "truststore_password_secure", trustStorePassword); + } + settingsBuilder.setSecureSettings(securitySettings); + } else { + settingsBuilder.put(sslConfigPrefix + "keystore_password", keyStorePassword); + settingsBuilder.put(sslConfigPrefix + "keystore_keypassword", certificatesRule.privateKeyPassword()); + if (useAuthorityCertificate) { + settingsBuilder.put(sslConfigPrefix + "truststore_password", trustStorePassword); + } + } + + final var configuration = new SslCertificatesLoader(sslConfigPrefix).loadConfiguration( + TestEnvironment.newEnvironment(settingsBuilder.build()) + ); + + if (useAuthorityCertificate) { + final var expectedTrustStoreCertificates = isNull(trustStoreAlias) + ? trustStoreCertificates.entrySet() + .stream() + .map(e -> new Certificate(e.getValue(), resolveKeyStoreType.apply(trustStoreType), e.getKey(), false)) + .toArray(Certificate[]::new) + : trustStoreCertificates.entrySet() + .stream() + .filter(e -> e.getKey().equals(trustStoreAlias)) + .map(e -> new Certificate(e.getValue(), resolveKeyStoreType.apply(trustStoreType), e.getKey(), false)) + .toArray(Certificate[]::new); + assertTrustStoreConfiguration(configuration.v1(), trustStorePath, expectedTrustStoreCertificates); + } else { + assertThat(configuration.v1(), is(TrustStoreConfiguration.EMPTY_CONFIGURATION)); + } + + final var expectedKeyStoreCertificates = isNull(keyStoreAlias) + ? keyStoreCertificates.entrySet() + .stream() + .map(e -> new Certificate(e.getValue().v2(), resolveKeyStoreType.apply(keyStoreType), e.getKey(), true)) + .toArray(Certificate[]::new) + : keyStoreCertificates.entrySet() + .stream() + .filter(e -> e.getKey().equals(keyStoreAlias)) + .map(e -> new Certificate(e.getValue().v2(), resolveKeyStoreType.apply(keyStoreType), e.getKey(), true)) + .toArray(Certificate[]::new); + assertKeyStoreConfiguration(configuration.v2(), List.of(keyStorePath), expectedKeyStoreCertificates); + } + + String randomKeyStoreType() { + return randomFrom(new String[] { "jks", "pkcs12", null }); + } + + String randomKeyStorePassword(final boolean useSecurePassword) { + return useSecurePassword ? randomAsciiAlphanumOfLength(10) : randomFrom(new String[] { randomAsciiAlphanumOfLength(10), null }); + } + + Path createTrustStore(final String type, final String password, Map certificates) throws Exception { + final var keyStore = keyStore(type); + for (final var alias : certificates.keySet()) { + keyStore.setCertificateEntry(alias, certificates.get(alias)); + } + final var trustStorePath = path(String.format("truststore.%s", isNull(type) ? "jsk" : type)); + storeKeyStore(keyStore, trustStorePath, password); + return trustStorePath; + } + + Path createKeyStore(final String type, final String password, final Map> keysAndCertificates) + throws Exception { + final var keyStore = keyStore(type); + final var keyStorePath = path(String.format("keystore.%s", isNull(type) ? "jsk" : type)); + for (final var alias : keysAndCertificates.keySet()) { + final var keyAndCertificate = keysAndCertificates.get(alias); + keyStore.setKeyEntry( + alias, + keyAndCertificate.v1(), + certificatesRule.privateKeyPassword().toCharArray(), + new X509Certificate[] { keyAndCertificate.v2() } + ); + } + storeKeyStore(keyStore, keyStorePath, password); + return keyStorePath; + } + + KeyStore keyStore(final String type) throws Exception { + final var keyStore = KeyStore.getInstance(isNull(type) ? KeyStore.getDefaultType() : type); + keyStore.load(null, null); + return keyStore; + } + + void storeKeyStore(final KeyStore keyStore, final Path path, final String password) throws Exception { + try (final var out = Files.newOutputStream(path)) { + keyStore.store(out, isNull(password) ? DEFAULT_STORE_PASSWORD.toCharArray() : password.toCharArray()); + } + } + +} diff --git a/src/test/java/org/opensearch/security/ssl/config/PemSslCertificatesLoaderTest.java b/src/test/java/org/opensearch/security/ssl/config/PemSslCertificatesLoaderTest.java new file mode 100644 index 0000000000..d03bf9c59d --- /dev/null +++ b/src/test/java/org/opensearch/security/ssl/config/PemSslCertificatesLoaderTest.java @@ -0,0 +1,174 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.ssl.config; + +import java.security.SecureRandom; +import java.util.List; + +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.BeforeClass; +import org.junit.Test; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.openssl.PKCS8Generator; +import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8EncryptorBuilder; + +import org.opensearch.common.settings.MockSecureSettings; +import org.opensearch.env.TestEnvironment; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.opensearch.security.ssl.CertificatesUtils.privateKeyToPemObject; +import static org.opensearch.security.ssl.CertificatesUtils.writePemContent; +import static org.opensearch.security.ssl.util.SSLConfigConstants.ENABLED; +import static org.opensearch.security.ssl.util.SSLConfigConstants.PEM_CERT_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.PEM_KEY_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.PEM_TRUSTED_CAS_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_CLIENT_PEMCERT_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_CLIENT_PEMKEY_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_CLIENT_PEMTRUSTEDCAS_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENABLED; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_SERVER_PEMCERT_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_SERVER_PEMKEY_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_SERVER_PEMTRUSTEDCAS_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_HTTP_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_TRANSPORT_CLIENT_EXTENDED_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_TRANSPORT_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_TRANSPORT_SERVER_EXTENDED_PREFIX; + +public class PemSslCertificatesLoaderTest extends SslCertificatesLoaderTest { + + final static String PEM_CA_CERTIFICATE_FILE_NAME = "ca_certificate.pem"; + + final static String PEM_KEY_CERTIFICATE_FILE_NAME = "key_certificate.pem"; + + final static String PEM_CERTIFICATE_PRIVATE_KEY_FILE_NAME = "private_key.pem"; + + @BeforeClass + public static void setup() throws Exception { + writePemContent(path(PEM_CA_CERTIFICATE_FILE_NAME), certificatesRule.caCertificateHolder()); + writePemContent(path(PEM_KEY_CERTIFICATE_FILE_NAME), certificatesRule.accessCertificateHolder()); + writePemContent( + path(PEM_CERTIFICATE_PRIVATE_KEY_FILE_NAME), + new PKCS8Generator( + PrivateKeyInfo.getInstance(certificatesRule.accessCertificatePrivateKey().getEncoded()), + new JceOpenSSLPKCS8EncryptorBuilder(PKCS8Generator.PBE_SHA1_3DES).setRandom(new SecureRandom()) + .setPassword(certificatesRule.privateKeyPassword().toCharArray()) + .build() + ).generate() + ); + } + + @Test + public void loadHttpSslConfigurationFromPemFiles() throws Exception { + testLoadPemBasedConfiguration(SSL_HTTP_PREFIX, randomBoolean()); + } + + @Test + public void loadTransportSslConfigurationFromPemFiles() throws Exception { + testLoadPemBasedConfiguration(SSL_HTTP_PREFIX, false); + } + + void testLoadPemBasedConfiguration(final String sslConfigPrefix, final boolean useAuthorityCertificate) throws Exception { + final var settingsBuilder = defaultSettingsBuilder().put(sslConfigPrefix + ENABLED, true) + .put(sslConfigPrefix + PEM_CERT_FILEPATH, path(PEM_KEY_CERTIFICATE_FILE_NAME)) + .put(sslConfigPrefix + PEM_KEY_FILEPATH, path(PEM_CERTIFICATE_PRIVATE_KEY_FILE_NAME)); + if (useAuthorityCertificate) { + settingsBuilder.put(sslConfigPrefix + PEM_TRUSTED_CAS_FILEPATH, path(PEM_CA_CERTIFICATE_FILE_NAME)); + } + if (randomBoolean()) { + final var securitySettings = new MockSecureSettings(); + securitySettings.setString(sslConfigPrefix + "pemkey_password_secure", certificatesRule.privateKeyPassword()); + settingsBuilder.setSecureSettings(securitySettings); + } else { + settingsBuilder.put(sslConfigPrefix + "pemkey_password", certificatesRule.privateKeyPassword()); + } + + final var settings = settingsBuilder.build(); + final var configuration = new SslCertificatesLoader(SSL_HTTP_PREFIX).loadConfiguration(TestEnvironment.newEnvironment(settings)); + if (useAuthorityCertificate) { + assertTrustStoreConfiguration( + configuration.v1(), + path(PEM_CA_CERTIFICATE_FILE_NAME), + new Certificate(certificatesRule.x509CaCertificate(), false) + ); + } else { + assertThat(configuration.v1(), is(TrustStoreConfiguration.EMPTY_CONFIGURATION)); + } + assertKeyStoreConfiguration( + configuration.v2(), + List.of(path(PEM_KEY_CERTIFICATE_FILE_NAME), path(PEM_CERTIFICATE_PRIVATE_KEY_FILE_NAME)), + new Certificate(certificatesRule.x509AccessCertificate(), true) + ); + } + + @Test + public void loadExtendedTransportSslConfigurationFromPemFiles() throws Exception { + final var keyPair = certificatesRule.generateKeyPair(); + final var clientCaCertificate = certificatesRule.generateCaCertificate(keyPair); + final var keyAndCertificate = certificatesRule.generateAccessCertificate(keyPair); + final var clientCaCertificatePath = "client_ca_certificate.pem"; + final var clientKeyCertificatePath = "client_key_certificate.pem"; + final var clientPrivateKeyCertificatePath = "client_private_key_certificate.pem"; + final var clientPrivateKeyPassword = RandomStringUtils.randomAlphabetic(10); + + writePemContent(path(clientCaCertificatePath), clientCaCertificate); + writePemContent(path(clientKeyCertificatePath), keyAndCertificate.v2()); + writePemContent(path(clientPrivateKeyCertificatePath), privateKeyToPemObject(keyAndCertificate.v1(), clientPrivateKeyPassword)); + + final var settingsBuilder = defaultSettingsBuilder().put(SECURITY_SSL_TRANSPORT_ENABLED, true) + .put(SECURITY_SSL_TRANSPORT_EXTENDED_KEY_USAGE_ENABLED, true) + .put(SECURITY_SSL_TRANSPORT_SERVER_PEMTRUSTEDCAS_FILEPATH, path(PEM_CA_CERTIFICATE_FILE_NAME)) + .put(SECURITY_SSL_TRANSPORT_SERVER_PEMCERT_FILEPATH, path(PEM_KEY_CERTIFICATE_FILE_NAME)) + .put(SECURITY_SSL_TRANSPORT_SERVER_PEMKEY_FILEPATH, path(PEM_CERTIFICATE_PRIVATE_KEY_FILE_NAME)) + + .put(SECURITY_SSL_TRANSPORT_CLIENT_PEMTRUSTEDCAS_FILEPATH, path(clientCaCertificatePath)) + .put(SECURITY_SSL_TRANSPORT_CLIENT_PEMCERT_FILEPATH, path(clientKeyCertificatePath)) + .put(SECURITY_SSL_TRANSPORT_CLIENT_PEMKEY_FILEPATH, path(clientPrivateKeyCertificatePath)); + if (randomBoolean()) { + final var securitySettings = new MockSecureSettings(); + securitySettings.setString(SSL_TRANSPORT_PREFIX + "server.pemkey_password_secure", certificatesRule.privateKeyPassword()); + securitySettings.setString(SSL_TRANSPORT_PREFIX + "client.pemkey_password_secure", clientPrivateKeyPassword); + settingsBuilder.setSecureSettings(securitySettings); + } else { + settingsBuilder.put(SSL_TRANSPORT_PREFIX + "server.pemkey_password", certificatesRule.privateKeyPassword()); + settingsBuilder.put(SSL_TRANSPORT_PREFIX + "client.pemkey_password", clientPrivateKeyPassword); + } + final var settings = settingsBuilder.build(); + + final var transportServerConfiguration = new SslCertificatesLoader(SSL_TRANSPORT_PREFIX, SSL_TRANSPORT_SERVER_EXTENDED_PREFIX) + .loadConfiguration(TestEnvironment.newEnvironment(settings)); + assertTrustStoreConfiguration( + transportServerConfiguration.v1(), + path(PEM_CA_CERTIFICATE_FILE_NAME), + new Certificate(certificatesRule.x509CaCertificate(), false) + ); + assertKeyStoreConfiguration( + transportServerConfiguration.v2(), + List.of(path(PEM_KEY_CERTIFICATE_FILE_NAME), path(PEM_CERTIFICATE_PRIVATE_KEY_FILE_NAME)), + new Certificate(certificatesRule.x509AccessCertificate(), true) + ); + final var transportClientConfiguration = new SslCertificatesLoader(SSL_TRANSPORT_PREFIX, SSL_TRANSPORT_CLIENT_EXTENDED_PREFIX) + .loadConfiguration(TestEnvironment.newEnvironment(settings)); + assertTrustStoreConfiguration( + transportClientConfiguration.v1(), + path(clientCaCertificatePath), + new Certificate(certificatesRule.toX509Certificate(clientCaCertificate), false) + ); + assertKeyStoreConfiguration( + transportClientConfiguration.v2(), + List.of(path(clientKeyCertificatePath), path(clientPrivateKeyCertificatePath)), + new Certificate(certificatesRule.toX509Certificate(keyAndCertificate.v2()), true) + ); + } + +} diff --git a/src/test/java/org/opensearch/security/ssl/config/SslCertificatesLoaderTest.java b/src/test/java/org/opensearch/security/ssl/config/SslCertificatesLoaderTest.java new file mode 100644 index 0000000000..0dfc02b386 --- /dev/null +++ b/src/test/java/org/opensearch/security/ssl/config/SslCertificatesLoaderTest.java @@ -0,0 +1,66 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.ssl.config; + +import java.nio.file.Path; +import java.util.List; + +import com.carrotsearch.randomizedtesting.RandomizedTest; +import org.junit.ClassRule; + +import org.opensearch.common.settings.Settings; +import org.opensearch.env.Environment; +import org.opensearch.security.ssl.CertificatesRule; + +import static java.util.Objects.nonNull; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.notNullValue; + +public abstract class SslCertificatesLoaderTest extends RandomizedTest { + + @ClassRule + public static CertificatesRule certificatesRule = new CertificatesRule(); + + static Path path(final String fileName) { + return certificatesRule.configRootFolder().resolve(fileName); + } + + Settings.Builder defaultSettingsBuilder() throws Exception { + return Settings.builder().put(Environment.PATH_HOME_SETTING.getKey(), certificatesRule.caCertificateHolder().toString()); + } + + void assertTrustStoreConfiguration( + final TrustStoreConfiguration trustStoreConfiguration, + final Path expectedFile, + final Certificate... expectedCertificates + ) { + assertThat("Truststore configuration created", nonNull(trustStoreConfiguration)); + assertThat(trustStoreConfiguration.file(), is(expectedFile)); + assertThat(trustStoreConfiguration.loadCertificates(), containsInAnyOrder(expectedCertificates)); + assertThat(trustStoreConfiguration.createTrustManagerFactory(true), is(notNullValue())); + } + + void assertKeyStoreConfiguration( + final KeyStoreConfiguration keyStoreConfiguration, + final List expectedFiles, + final Certificate... expectedCertificates + ) { + assertThat("Keystore configuration created", nonNull(keyStoreConfiguration)); + assertThat(keyStoreConfiguration.files(), contains(expectedFiles.toArray(new Path[0]))); + assertThat(keyStoreConfiguration.loadCertificates(), containsInAnyOrder(expectedCertificates)); + assertThat(keyStoreConfiguration.createKeyManagerFactory(true), is(notNullValue())); + } + +} diff --git a/src/test/java/org/opensearch/security/ssl/config/SslParametersTest.java b/src/test/java/org/opensearch/security/ssl/config/SslParametersTest.java new file mode 100644 index 0000000000..d95c336e15 --- /dev/null +++ b/src/test/java/org/opensearch/security/ssl/config/SslParametersTest.java @@ -0,0 +1,90 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.ssl.config; + +import java.util.List; +import java.util.Locale; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.net.ssl.SSLContext; + +import org.junit.Test; + +import org.opensearch.common.settings.Settings; + +import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.SslProvider; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.opensearch.security.ssl.util.SSLConfigConstants.ALLOWED_SSL_CIPHERS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_CLIENTAUTH_MODE; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED_CIPHERS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED_PROTOCOLS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENABLED_CIPHERS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENABLED_PROTOCOLS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_HTTP_PREFIX; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_TRANSPORT_PREFIX; + +public class SslParametersTest { + + @Test + public void testDefaultSslParameters() throws Exception { + final var settings = Settings.EMPTY; + final var httpSslParameters = SslParameters.loader(settings).load(true); + final var transportSslParameters = SslParameters.loader(settings).load(false); + + final var defaultCiphers = List.of(ALLOWED_SSL_CIPHERS); + final var finalDefaultCiphers = Stream.of(SSLContext.getDefault().getDefaultSSLParameters().getCipherSuites()) + .filter(defaultCiphers::contains) + .sorted(String::compareTo) + .collect(Collectors.toList()); + + assertThat(httpSslParameters.provider(), is(SslProvider.JDK)); + assertThat(transportSslParameters.provider(), is(SslProvider.JDK)); + + assertThat(httpSslParameters.allowedProtocols(), is(List.of("TLSv1.3", "TLSv1.2"))); + assertThat(httpSslParameters.allowedCiphers(), is(finalDefaultCiphers)); + + assertThat(transportSslParameters.allowedProtocols(), is(List.of("TLSv1.3", "TLSv1.2"))); + assertThat(transportSslParameters.allowedCiphers(), is(finalDefaultCiphers)); + + assertThat(httpSslParameters.clientAuth(), is(ClientAuth.OPTIONAL)); + assertThat(transportSslParameters.clientAuth(), is(ClientAuth.REQUIRE)); + } + + @Test + public void testCustomSSlParameters() { + final var settings = Settings.builder() + .put(SECURITY_SSL_HTTP_CLIENTAUTH_MODE, ClientAuth.REQUIRE.name().toLowerCase(Locale.ROOT)) + .putList(SECURITY_SSL_HTTP_ENABLED_PROTOCOLS, List.of("TLSv1.2", "TLSv1")) + .putList(SECURITY_SSL_HTTP_ENABLED_CIPHERS, List.of("TLS_AES_256_GCM_SHA384")) + .putList(SECURITY_SSL_TRANSPORT_ENABLED_PROTOCOLS, List.of("TLSv1.3", "TLSv1.2")) + .putList(SECURITY_SSL_TRANSPORT_ENABLED_CIPHERS, List.of("TLS_AES_128_GCM_SHA256", "TLS_AES_256_GCM_SHA384")) + .build(); + final var httpSslParameters = SslParameters.loader(settings.getByPrefix(SSL_HTTP_PREFIX)).load(true); + final var transportSslParameters = SslParameters.loader(settings.getByPrefix(SSL_TRANSPORT_PREFIX)).load(false); + + assertThat(httpSslParameters.provider(), is(SslProvider.JDK)); + assertThat(transportSslParameters.provider(), is(SslProvider.JDK)); + + assertThat(httpSslParameters.allowedProtocols(), is(List.of("TLSv1.2"))); + assertThat(httpSslParameters.allowedCiphers(), is(List.of("TLS_AES_256_GCM_SHA384"))); + + assertThat(transportSslParameters.allowedProtocols(), is(List.of("TLSv1.3", "TLSv1.2"))); + assertThat(transportSslParameters.allowedCiphers(), is(List.of("TLS_AES_128_GCM_SHA256", "TLS_AES_256_GCM_SHA384"))); + + assertThat(httpSslParameters.clientAuth(), is(ClientAuth.REQUIRE)); + assertThat(transportSslParameters.clientAuth(), is(ClientAuth.REQUIRE)); + } + +} From 8b71209e8452212fdd88839bd8cbbc20524ad137 Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Tue, 22 Oct 2024 15:27:04 -0400 Subject: [PATCH 14/16] Add release notes for 2.18 (#4834) Signed-off-by: Derek Ho --- ...nsearch-security.release-notes-2.18.0.0.md | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 release-notes/opensearch-security.release-notes-2.18.0.0.md diff --git a/release-notes/opensearch-security.release-notes-2.18.0.0.md b/release-notes/opensearch-security.release-notes-2.18.0.0.md new file mode 100644 index 0000000000..b6faf32235 --- /dev/null +++ b/release-notes/opensearch-security.release-notes-2.18.0.0.md @@ -0,0 +1,48 @@ +## Version 2.18.0 Release Notes + +Compatible with OpenSearch and OpenSearch Dashboards version 2.18.0 + +### Enhancements +* Improve error message when a node with an incorrectly configured certificate attempts to connect ([#4819](https://github.com/opensearch-project/security/pull/4819)) +* Support datastreams as an AuditLog Sink ([#4756](https://github.com/opensearch-project/security/pull/4756)) +* Auto-convert V6 configuration instances into V7 configuration instances (for OpenSearch 2.x only) ([#4753](https://github.com/opensearch-project/security/pull/4753)) +* Add can trip circuit breaker override ([#4779](https://github.com/opensearch-project/security/pull/4779)) +* Adding index permissions for remote index in AD ([#4721](https://github.com/opensearch-project/security/pull/4721)) +* Fix env var password hashing for PBKDF2 ([#4778](https://github.com/opensearch-project/security/pull/4778)) +* Add ensureCustomSerialization to ensure that headers are serialized correctly with multiple transport hops ([#4741](https://github.com/opensearch-project/security/pull/4741)) + +### Bug Fixes +* Handle non-flat yaml settings for demo configuration detection ([#4798](https://github.com/opensearch-project/security/pull/4798)) +* Fix bug where admin can read system index ([#4775](https://github.com/opensearch-project/security/pull/4775)) +* Ensure that dual mode enabled flag from cluster settings can get propagated to core ([#4830](https://github.com/opensearch-project/security/pull/4830)) +* Remove failed login attempt for saml authenticator ([#4770](https://github.com/opensearch-project/security/pull/4770)) +* Fix issue in HashingStoredFieldVisitor with stored fields ([#4827](https://github.com/opensearch-project/security/pull/4827)) +* Fix issue with Get mappings on a Closed index ([#4777](https://github.com/opensearch-project/security/pull/4777)) +* changing comments permission for alerting_ack_alerts role ([#4723](https://github.com/opensearch-project/security/pull/4723)) +* Fixed use of rolesMappingConfiguration in InternalUsersApiActionValidationTest ([#4754](https://github.com/opensearch-project/security/pull/4754)) +* Use evaluateSslExceptionHandler() when constructing OpenSearchSecureSettingsFactory ([#4726](https://github.com/opensearch-project/security/pull/4726)) + +### Maintenance +* Bump gradle to 8.10.2 ([#4829](https://github.com/opensearch-project/security/pull/4829)) +* Bump ch.qos.logback:logback-classic from 1.5.8 to 1.5.11 ([#4807](https://github.com/opensearch-project/security/pull/4807)) ([#4825](https://github.com/opensearch-project/security/pull/4825)) +* Bump org.passay:passay from 1.6.5 to 1.6.6 ([#4824](https://github.com/opensearch-project/security/pull/4824)) +* Bump org.junit.jupiter:junit-jupiter from 5.11.0 to 5.11.2 ([#4767](https://github.com/opensearch-project/security/pull/4767)) ([#4811](https://github.com/opensearch-project/security/pull/4811)) +* Bump io.dropwizard.metrics:metrics-core from 4.2.27 to 4.2.28 ([#4789](https://github.com/opensearch-project/security/pull/4789)) +* Bump com.nimbusds:nimbus-jose-jwt from 9.40 to 9.41.2 ([#4737](https://github.com/opensearch-project/security/pull/4737)) ([#4787](https://github.com/opensearch-project/security/pull/4787)) +* Bump org.ow2.asm:asm from 9.7 to 9.7.1 ([#4788](https://github.com/opensearch-project/security/pull/4788)) +* Bump com.google.googlejavaformat:google-java-format from 1.23.0 to 1.24.0 ([#4786](https://github.com/opensearch-project/security/pull/4786)) +* Bump org.xerial.snappy:snappy-java from 1.1.10.6 to 1.1.10.7 ([#4738](https://github.com/opensearch-project/security/pull/4738)) +* Bump org.gradle.test-retry from 1.5.10 to 1.6.0 ([#4736](https://github.com/opensearch-project/security/pull/4736)) +* Moves @cliu123 to emeritus status ([#4667](https://github.com/opensearch-project/security/pull/4667)) +* Add Derek Ho (github: derek-ho) as a maintainer ([#4796](https://github.com/opensearch-project/security/pull/4796)) +* Add deprecation warning for GET/POST/PUT cache ([#4776](https://github.com/opensearch-project/security/pull/4776)) +* Fix for: CVE-2024-47554 ([#4792](https://github.com/opensearch-project/security/pull/4792)) +* Move Stephen to emeritus ([#4804](https://github.com/opensearch-project/security/pull/4804)) +* Undeprecate securityadmin script ([#4768](https://github.com/opensearch-project/security/pull/4768)) +* Bump commons-io:commons-io from 2.16.1 to 2.17.0 ([#4750](https://github.com/opensearch-project/security/pull/4750)) +* Bump org.scala-lang:scala-library from 2.13.14 to 2.13.15 ([#4749](https://github.com/opensearch-project/security/pull/4749)) +* org.checkerframework:checker-qual and ch.qos.logback:logback-classic to new versions ([#4717](https://github.com/opensearch-project/security/pull/4717)) +* Add isActionPaginated to DelegatingRestHandler ([#4765](https://github.com/opensearch-project/security/pull/4765)) +* Refactor ASN1 call ([#4740](https://github.com/opensearch-project/security/pull/4740)) +* Fix 'integTest' not called with test workflows during release ([#4815](https://github.com/opensearch-project/security/pull/4815)) +* Fixed bulk index requests in BWC tests and hardened assertions ([#4831](https://github.com/opensearch-project/security/pull/4831)) From eb7f8218be7ed49da4d76fbb22b7b473847c4068 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Wed, 23 Oct 2024 13:59:21 -0400 Subject: [PATCH 15/16] Generalize public key reading in the JWT authenticator (#4833) Signed-off-by: Craig Perkins --- .../opensearch/security/util/KeyUtils.java | 4 +- .../http/jwt/HTTPJwtAuthenticatorTest.java | 72 +++++++++++++------ 2 files changed, 51 insertions(+), 25 deletions(-) diff --git a/src/main/java/org/opensearch/security/util/KeyUtils.java b/src/main/java/org/opensearch/security/util/KeyUtils.java index 920cf198be..bb2abea795 100644 --- a/src/main/java/org/opensearch/security/util/KeyUtils.java +++ b/src/main/java/org/opensearch/security/util/KeyUtils.java @@ -52,8 +52,8 @@ public JwtParserBuilder run() { } else { try { PublicKey key = null; - - final String minimalKeyFormat = signingKey.replace("-----BEGIN PUBLIC KEY-----\n", "") + final String minimalKeyFormat = signingKey.replaceAll("\\r|\\n", "") + .replace("-----BEGIN PUBLIC KEY-----", "") .replace("-----END PUBLIC KEY-----", "") .trim(); final byte[] decoded = Base64.getDecoder().decode(minimalKeyFormat); diff --git a/src/test/java/com/amazon/dlic/auth/http/jwt/HTTPJwtAuthenticatorTest.java b/src/test/java/com/amazon/dlic/auth/http/jwt/HTTPJwtAuthenticatorTest.java index 4214e8ed06..48a14916a0 100644 --- a/src/test/java/com/amazon/dlic/auth/http/jwt/HTTPJwtAuthenticatorTest.java +++ b/src/test/java/com/amazon/dlic/auth/http/jwt/HTTPJwtAuthenticatorTest.java @@ -389,7 +389,6 @@ public void testNbf() throws Exception { @Test public void testRS256() throws Exception { - KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); keyGen.initialize(2048); KeyPair pair = keyGen.generateKeyPair(); @@ -397,27 +396,61 @@ public void testRS256() throws Exception { PublicKey pub = pair.getPublic(); String jwsToken = Jwts.builder().setSubject("Leonard McCoy").signWith(priv, SignatureAlgorithm.RS256).compact(); - Settings settings = Settings.builder() - .put( - "signing_key", - "-----BEGIN PUBLIC KEY-----\n" + BaseEncoding.base64().encode(pub.getEncoded()) + "-----END PUBLIC KEY-----" - ) - .build(); + String signingKey = "-----BEGIN PUBLIC KEY-----\n" + BaseEncoding.base64().encode(pub.getEncoded()) + "-----END PUBLIC KEY-----"; + AuthCredentials creds = testJwtAuthenticationWithSigningKey(signingKey, jwsToken); - HTTPJwtAuthenticator jwtAuth = new HTTPJwtAuthenticator(settings, null); - Map headers = new HashMap(); - headers.put("Authorization", "Bearer " + jwsToken); + Assert.assertNotNull(creds); + assertThat(creds.getUsername(), is("Leonard McCoy")); + assertThat(creds.getBackendRoles().size(), is(0)); + } - AuthCredentials creds = jwtAuth.extractCredentials( - new FakeRestRequest(headers, new HashMap()).asSecurityRequest(), - null - ); + private static String formatKeyWithNewlines(String keyAsString) { + StringBuilder result = new StringBuilder(); + int lineLength = 64; + int length = keyAsString.length(); + + for (int i = 0; i < length; i += lineLength) { + if (i + lineLength < length) { + result.append(keyAsString, i, i + lineLength); + } else { + result.append(keyAsString.substring(i)); + } + result.append("\n"); + } + + return result.toString().trim(); + } + + @Test + public void testRS256WithNewlines() throws Exception { + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + keyGen.initialize(2048); + KeyPair pair = keyGen.generateKeyPair(); + PrivateKey priv = pair.getPrivate(); + PublicKey pub = pair.getPublic(); + + String jwsToken = Jwts.builder().setSubject("Leonard McCoy").signWith(priv, SignatureAlgorithm.RS256).compact(); + + String signingKey = "-----BEGIN PUBLIC KEY-----\n" + + formatKeyWithNewlines(BaseEncoding.base64().encode(pub.getEncoded())) + + "\n-----END PUBLIC KEY-----"; + AuthCredentials creds = testJwtAuthenticationWithSigningKey(signingKey, jwsToken); Assert.assertNotNull(creds); assertThat(creds.getUsername(), is("Leonard McCoy")); assertThat(creds.getBackendRoles().size(), is(0)); } + private AuthCredentials testJwtAuthenticationWithSigningKey(String signingKey, String jwsToken) throws NoSuchAlgorithmException { + Settings settings = Settings.builder().put("signing_key", signingKey).build(); + + HTTPJwtAuthenticator jwtAuth = new HTTPJwtAuthenticator(settings, null); + Map headers = new HashMap(); + headers.put("Authorization", "Bearer " + jwsToken); + + return jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap()).asSecurityRequest(), null); + } + @Test public void testES512() throws Exception { @@ -427,17 +460,10 @@ public void testES512() throws Exception { PrivateKey priv = pair.getPrivate(); PublicKey pub = pair.getPublic(); - Settings settings = Settings.builder().put("signing_key", BaseEncoding.base64().encode(pub.getEncoded())).build(); + String signingKey = BaseEncoding.base64().encode(pub.getEncoded()); String jwsToken = Jwts.builder().setSubject("Leonard McCoy").signWith(priv, SignatureAlgorithm.ES512).compact(); - HTTPJwtAuthenticator jwtAuth = new HTTPJwtAuthenticator(settings, null); - Map headers = new HashMap(); - headers.put("Authorization", jwsToken); - - AuthCredentials creds = jwtAuth.extractCredentials( - new FakeRestRequest(headers, new HashMap()).asSecurityRequest(), - null - ); + AuthCredentials creds = testJwtAuthenticationWithSigningKey(signingKey, jwsToken); Assert.assertNotNull(creds); assertThat(creds.getUsername(), is("Leonard McCoy")); From c8cacf1259a611934da6aa631a65d5ed0fe3d216 Mon Sep 17 00:00:00 2001 From: parisl <11188036+parislarkins@users.noreply.github.com> Date: Thu, 24 Oct 2024 05:38:25 +1100 Subject: [PATCH 16/16] Allow skipping hot reload dn validation (#4752) Signed-off-by: Paris Larkins Signed-off-by: Paris Larkins --- .../security/ssl/DefaultSecurityKeyStore.java | 31 ++- .../ssl/OpenSearchSecuritySSLPlugin.java | 17 ++ .../security/ssl/util/SSLConfigConstants.java | 4 + .../SecuritySSLReloadCertsActionTests.java | 217 +++++++++++++++--- src/test/resources/ssl/reload/README.txt | 29 +++ .../resources/ssl/reload/node-new-ca.crt.pem | 82 +++++++ .../resources/ssl/reload/node-new-ca.key.pem | 28 +++ .../ssl/reload/secondary-root-ca.pem | 29 +++ .../ssl/reload/secondary-signing-ca.pem | 29 +++ src/test/resources/ssl/reload/truststore.jks | Bin 1398 -> 4230 bytes 10 files changed, 432 insertions(+), 34 deletions(-) create mode 100644 src/test/resources/ssl/reload/README.txt create mode 100644 src/test/resources/ssl/reload/node-new-ca.crt.pem create mode 100644 src/test/resources/ssl/reload/node-new-ca.key.pem create mode 100644 src/test/resources/ssl/reload/secondary-root-ca.pem create mode 100644 src/test/resources/ssl/reload/secondary-signing-ca.pem diff --git a/src/main/java/org/opensearch/security/ssl/DefaultSecurityKeyStore.java b/src/main/java/org/opensearch/security/ssl/DefaultSecurityKeyStore.java index 8dbd2f139a..61dac199ae 100644 --- a/src/main/java/org/opensearch/security/ssl/DefaultSecurityKeyStore.java +++ b/src/main/java/org/opensearch/security/ssl/DefaultSecurityKeyStore.java @@ -133,7 +133,9 @@ private void printJCEWarnings() { public final SslProvider sslTransportServerProvider; public final SslProvider sslTransportClientProvider; private final boolean httpSSLEnabled; + private final boolean httpSSLEnforceCertReloadDnVerification; private final boolean transportSSLEnabled; + private final boolean transportSSLEnforceCertReloadDnVerification; private ArrayList enabledHttpCiphersJDKProvider; private ArrayList enabledHttpCiphersOpenSSLProvider; @@ -166,10 +168,18 @@ public DefaultSecurityKeyStore(final Settings settings, final Path configPath) { SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED, SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED_DEFAULT ); + httpSSLEnforceCertReloadDnVerification = settings.getAsBoolean( + SSLConfigConstants.SECURITY_SSL_HTTP_ENFORCE_CERT_RELOAD_DN_VERIFICATION, + true + ); transportSSLEnabled = settings.getAsBoolean( SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENABLED, SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENABLED_DEFAULT ); + transportSSLEnforceCertReloadDnVerification = settings.getAsBoolean( + SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENFORCE_CERT_RELOAD_DN_VERIFICATION, + true + ); final boolean useOpenSSLForHttpIfAvailable = OpenSearchSecuritySSLPlugin.OPENSSL_SUPPORTED && settings.getAsBoolean(SSLConfigConstants.SECURITY_SSL_HTTP_ENABLE_OPENSSL_IF_AVAILABLE, true); final boolean useOpenSSLForTransportIfAvailable = OpenSearchSecuritySSLPlugin.OPENSSL_SUPPORTED @@ -422,7 +432,7 @@ public void initTransportSSLConfig() { certFromTruststore = new CertFromTruststore(truststoreProps, truststoreAlias); } - validateNewCerts(transportCerts, certFromKeystore.getCerts()); + validateNewCerts(transportCerts, certFromKeystore.getCerts(), transportSSLEnforceCertReloadDnVerification); transportServerSslContext = buildSSLServerContext( certFromKeystore.getServerKey(), certFromKeystore.getServerCert(), @@ -473,7 +483,7 @@ public void initTransportSSLConfig() { certFromFile = new CertFromFile(certProps); } - validateNewCerts(transportCerts, certFromFile.getCerts()); + validateNewCerts(transportCerts, certFromFile.getCerts(), transportSSLEnforceCertReloadDnVerification); transportServerSslContext = buildSSLServerContext( certFromFile.getServerPemKey(), certFromFile.getServerPemCert(), @@ -571,7 +581,7 @@ public void initHttpSSLConfig() { certFromTruststore = new CertFromTruststore(truststoreProps, truststoreAlias); } - validateNewCerts(httpCerts, certFromKeystore.getCerts()); + validateNewCerts(httpCerts, certFromKeystore.getCerts(), httpSSLEnforceCertReloadDnVerification); httpSslContext = buildSSLServerContext( certFromKeystore.getServerKey(), certFromKeystore.getServerCert(), @@ -602,7 +612,7 @@ public void initHttpSSLConfig() { ); CertFromFile certFromFile = new CertFromFile(certFileProps); - validateNewCerts(httpCerts, certFromFile.getCerts()); + validateNewCerts(httpCerts, certFromFile.getCerts(), httpSSLEnforceCertReloadDnVerification); httpSslContext = buildSSLServerContext( certFromFile.getServerPemKey(), certFromFile.getServerPemCert(), @@ -633,11 +643,16 @@ public void initHttpSSLConfig() { * If the current and new certificates are same, skip remaining checks. * For new X509 cert to be valid Issuer, Subject DN must be the same and * new certificates should expire after current ones. - * @param currentX509Certs Array of current x509 certificates - * @param newX509Certs Array of x509 certificates which will replace our current cert + * @param currentX509Certs Array of current x509 certificates + * @param newX509Certs Array of x509 certificates which will replace our current cert + * @param verifyValidDNs Whether to verify that new certs have valid IssuerDN, SubjectDN and SAN * @throws Exception if certificate is invalid */ - private void validateNewCerts(final X509Certificate[] currentX509Certs, final X509Certificate[] newX509Certs) throws Exception { + private void validateNewCerts( + final X509Certificate[] currentX509Certs, + final X509Certificate[] newX509Certs, + final boolean verifyValidDNs + ) throws Exception { // First time we init certs ignore validity check if (currentX509Certs == null) { @@ -654,7 +669,7 @@ private void validateNewCerts(final X509Certificate[] currentX509Certs, final X5 } // Check if new X509 certs have valid IssuerDN, SubjectDN or SAN - if (!hasValidDNs(currentX509Certs, newX509Certs)) { + if (verifyValidDNs && !hasValidDNs(currentX509Certs, newX509Certs)) { throw new Exception("New Certs do not have valid Issuer DN, Subject DN or SAN."); } } diff --git a/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java b/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java index 25c55f3cbb..c12424f028 100644 --- a/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java +++ b/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java @@ -634,6 +634,23 @@ public List> getSettings() { Setting.longSetting(SSLConfigConstants.SECURITY_SSL_HTTP_CRL_VALIDATION_DATE, -1, -1, Property.NodeScope, Property.Filtered) ); + settings.add( + Setting.boolSetting( + SSLConfigConstants.SECURITY_SSL_HTTP_ENFORCE_CERT_RELOAD_DN_VERIFICATION, + true, + Property.NodeScope, + Property.Filtered + ) + ); + settings.add( + Setting.boolSetting( + SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENFORCE_CERT_RELOAD_DN_VERIFICATION, + true, + Property.NodeScope, + Property.Filtered + ) + ); + return settings; } diff --git a/src/main/java/org/opensearch/security/ssl/util/SSLConfigConstants.java b/src/main/java/org/opensearch/security/ssl/util/SSLConfigConstants.java index dfc9ae567e..003c46b093 100644 --- a/src/main/java/org/opensearch/security/ssl/util/SSLConfigConstants.java +++ b/src/main/java/org/opensearch/security/ssl/util/SSLConfigConstants.java @@ -82,6 +82,8 @@ public final class SSLConfigConstants { public static final String SECURITY_SSL_HTTP_TRUSTSTORE_ALIAS = "plugins.security.ssl.http.truststore_alias"; public static final String SECURITY_SSL_HTTP_TRUSTSTORE_FILEPATH = "plugins.security.ssl.http.truststore_filepath"; public static final String SECURITY_SSL_HTTP_TRUSTSTORE_TYPE = "plugins.security.ssl.http.truststore_type"; + public static final String SECURITY_SSL_HTTP_ENFORCE_CERT_RELOAD_DN_VERIFICATION = + "plugins.security.ssl.http.enforce_cert_reload_dn_verification"; public static final String SECURITY_SSL_TRANSPORT_ENABLE_OPENSSL_IF_AVAILABLE = "plugins.security.ssl.transport.enable_openssl_if_available"; public static final String SECURITY_SSL_TRANSPORT_ENABLED = "plugins.security.ssl.transport.enabled"; @@ -91,6 +93,8 @@ public final class SSLConfigConstants { public static final String SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME = "plugins.security.ssl.transport.resolve_hostname"; + public static final String SECURITY_SSL_TRANSPORT_ENFORCE_CERT_RELOAD_DN_VERIFICATION = + "plugins.security.ssl.transport.enforce_cert_reload_dn_verification"; public static final String SECURITY_SSL_TRANSPORT_KEYSTORE_ALIAS = "plugins.security.ssl.transport.keystore_alias"; public static final String SECURITY_SSL_TRANSPORT_SERVER_KEYSTORE_ALIAS = "plugins.security.ssl.transport.server.keystore_alias"; public static final String SECURITY_SSL_TRANSPORT_CLIENT_KEYSTORE_ALIAS = "plugins.security.ssl.transport.client.keystore_alias"; diff --git a/src/test/java/org/opensearch/security/ssl/SecuritySSLReloadCertsActionTests.java b/src/test/java/org/opensearch/security/ssl/SecuritySSLReloadCertsActionTests.java index 30635477eb..d3b428e9b2 100644 --- a/src/test/java/org/opensearch/security/ssl/SecuritySSLReloadCertsActionTests.java +++ b/src/test/java/org/opensearch/security/ssl/SecuritySSLReloadCertsActionTests.java @@ -14,7 +14,6 @@ import java.io.IOException; import java.util.List; import java.util.Map; -import java.util.Objects; import com.fasterxml.jackson.databind.JsonNode; import org.junit.After; @@ -44,10 +43,8 @@ public class SecuritySSLReloadCertsActionTests extends SingleClusterTest { private final String RELOAD_HTTP_CERTS_ENDPOINT = "_opendistro/_security/api/ssl/http/reloadcerts"; @Rule public TemporaryFolder testFolder = new TemporaryFolder(); - private final String HTTP_CERTIFICATES_LIST_KEY = "http_certificates_list"; - private final String TRANSPORT_CERTIFICATES_LIST_KEY = "transport_certificates_list"; - private final List> NODE_CERT_DETAILS = List.of( + private final List> INITIAL_NODE_CERT_DETAILS = List.of( Map.of( "issuer_dn", "CN=Example Com Inc. Signing CA,OU=Example Com Inc. Signing CA,O=Example Com Inc.,DC=example,DC=com", @@ -77,6 +74,21 @@ public class SecuritySSLReloadCertsActionTests extends SingleClusterTest { ) ); + private final List> NEW_CA_NODE_CERT_DETAILS = List.of( + Map.of( + "issuer_dn", + "CN=Example Com Inc. Secondary Signing CA,OU=Example Com Inc. Secondary Signing CA,O=Example Com Inc.,DC=example,DC=com", + "subject_dn", + "CN=node-1.example.com,OU=SSL,O=Test,L=Test,C=DE", + "san", + "[[2, localhost], [2, node-1.example.com], [7, 127.0.0.1], [8, 1.2.3.4.5.5]]", + "not_before", + "2024-09-17T00:15:48Z", + "not_after", + "2034-09-15T00:15:48Z" + ) + ); + private String pemCertFilePath; private String pemKeyFilePath; private final String defaultCertFilePath = "ssl/reload/node.crt.pem"; @@ -116,7 +128,7 @@ public void testReloadTransportSSLCertsPass() throws Exception { updateFiles(newCertFilePath, pemCertFilePath); updateFiles(newKeyFilePath, pemKeyFilePath); - assertReloadCertificateSuccess(rh, "transport", getUpdatedCertDetailsExpectedResponse("transport")); + assertReloadCertificateSuccess(rh, "transport", getCertDetailsExpectedResponse(INITIAL_NODE_CERT_DETAILS, NEW_NODE_CERT_DETAILS)); } @Test @@ -133,7 +145,7 @@ public void testReloadHttpSSLCertsPass() throws Exception { updateFiles(newCertFilePath, pemCertFilePath); updateFiles(newKeyFilePath, pemKeyFilePath); - assertReloadCertificateSuccess(rh, "http", getUpdatedCertDetailsExpectedResponse("http")); + assertReloadCertificateSuccess(rh, "http", getCertDetailsExpectedResponse(NEW_NODE_CERT_DETAILS, INITIAL_NODE_CERT_DETAILS)); } @Test @@ -189,6 +201,148 @@ public void testReloadHttpSSLSameCertsPass() throws Exception { assertReloadCertificateSuccess(rh, "http", getInitCertDetailsExpectedResponse()); } + @Test + public void testReloadHttpCertDifferentTrustChain_skipDnValidationPass() throws Exception { + updateFiles(defaultCertFilePath, pemCertFilePath); + updateFiles(defaultKeyFilePath, pemKeyFilePath); + initTestCluster(pemCertFilePath, pemKeyFilePath, pemCertFilePath, pemKeyFilePath, true, false, true); + + RestHelper rh = getRestHelperAdminUser(); + // Change http certs to one signed by a different CA than the previous one + updateFiles("ssl/reload/node-new-ca.crt.pem", pemCertFilePath); + updateFiles("ssl/reload/node-new-ca.key.pem", pemKeyFilePath); + + RestHelper.HttpResponse reloadCertsResponse = rh.executePutRequest(RELOAD_HTTP_CERTS_ENDPOINT, null); + + assertThat(reloadCertsResponse.getStatusCode(), is(200)); + final var expectedJsonResponse = DefaultObjectMapper.objectMapper.createObjectNode(); + expectedJsonResponse.put("message", "updated http certs"); + assertThat(reloadCertsResponse.getBody(), is(expectedJsonResponse.toString())); + + String certDetailsResponse = rh.executeSimpleRequest(GET_CERT_DETAILS_ENDPOINT); + assertThat( + DefaultObjectMapper.readTree(certDetailsResponse), + is(getCertDetailsExpectedResponse(NEW_CA_NODE_CERT_DETAILS, INITIAL_NODE_CERT_DETAILS)) + ); + } + + @Test + public void testReloadHttpCertDifferentTrustChain_noSkipDnValidationFail() throws Exception { + updateFiles(defaultCertFilePath, pemCertFilePath); + updateFiles(defaultKeyFilePath, pemKeyFilePath); + initTestCluster(pemCertFilePath, pemKeyFilePath, pemCertFilePath, pemKeyFilePath, true, true, true); + + RestHelper rh = getRestHelperAdminUser(); + // Change http certs to one signed by a different CA than the previous one + updateFiles("ssl/reload/node-new-ca.crt.pem", pemCertFilePath); + updateFiles("ssl/reload/node-new-ca.key.pem", pemKeyFilePath); + + RestHelper.HttpResponse reloadCertsResponse = rh.executePutRequest(RELOAD_HTTP_CERTS_ENDPOINT, null); + + assertThat(reloadCertsResponse.getStatusCode(), is(500)); + assertThat( + DefaultObjectMapper.readTree(reloadCertsResponse.getBody()).get("error").get("root_cause").get(0).get("reason").asText(), + is( + "OpenSearchSecurityException[Error while initializing http SSL layer from PEM: java.lang.Exception: " + + "New Certs do not have valid Issuer DN, Subject DN or SAN.]; nested: Exception[New Certs do not have valid Issuer DN, Subject DN or SAN.];" + ) + ); + } + + @Test + public void testReloadHttpCertDifferentTrustChain_defaultSettingValidationFail() throws Exception { + updateFiles(defaultCertFilePath, pemCertFilePath); + updateFiles(defaultKeyFilePath, pemKeyFilePath); + initTestCluster(pemCertFilePath, pemKeyFilePath, pemCertFilePath, pemKeyFilePath, true, null, null); + + RestHelper rh = getRestHelperAdminUser(); + // Change http certs to one signed by a different CA than the previous one + updateFiles("ssl/reload/node-new-ca.crt.pem", pemCertFilePath); + updateFiles("ssl/reload/node-new-ca.key.pem", pemKeyFilePath); + + RestHelper.HttpResponse reloadCertsResponse = rh.executePutRequest(RELOAD_HTTP_CERTS_ENDPOINT, null); + + assertThat(reloadCertsResponse.getStatusCode(), is(500)); + assertThat( + DefaultObjectMapper.readTree(reloadCertsResponse.getBody()).get("error").get("root_cause").get(0).get("reason").asText(), + is( + "OpenSearchSecurityException[Error while initializing http SSL layer from PEM: java.lang.Exception: " + + "New Certs do not have valid Issuer DN, Subject DN or SAN.]; nested: Exception[New Certs do not have valid Issuer DN, Subject DN or SAN.];" + ) + ); + } + + @Test + public void testReloadTransportCertDifferentTrustChain_skipDnValidationPass() throws Exception { + updateFiles(defaultCertFilePath, pemCertFilePath); + updateFiles(defaultKeyFilePath, pemKeyFilePath); + initTestCluster(pemCertFilePath, pemKeyFilePath, pemCertFilePath, pemKeyFilePath, true, true, false); + + RestHelper rh = getRestHelperAdminUser(); + // Change transport certs to one signed by a different CA than the previous one + updateFiles("ssl/reload/node-new-ca.crt.pem", pemCertFilePath); + updateFiles("ssl/reload/node-new-ca.key.pem", pemKeyFilePath); + + RestHelper.HttpResponse reloadCertsResponse = rh.executePutRequest(RELOAD_TRANSPORT_CERTS_ENDPOINT, null); + + assertThat(reloadCertsResponse.getStatusCode(), is(200)); + final var expectedJsonResponse = DefaultObjectMapper.objectMapper.createObjectNode(); + expectedJsonResponse.put("message", "updated transport certs"); + assertThat(reloadCertsResponse.getBody(), is(expectedJsonResponse.toString())); + + String certDetailsResponse = rh.executeSimpleRequest(GET_CERT_DETAILS_ENDPOINT); + assertThat( + DefaultObjectMapper.readTree(certDetailsResponse), + is(getCertDetailsExpectedResponse(INITIAL_NODE_CERT_DETAILS, NEW_CA_NODE_CERT_DETAILS)) + ); + } + + @Test + public void testReloadTransportCertDifferentTrustChain_noSkipDnValidationFail() throws Exception { + updateFiles(defaultCertFilePath, pemCertFilePath); + updateFiles(defaultKeyFilePath, pemKeyFilePath); + initTestCluster(pemCertFilePath, pemKeyFilePath, pemCertFilePath, pemKeyFilePath, true, true, true); + + RestHelper rh = getRestHelperAdminUser(); + // Change transport certs to one signed by a different CA than the previous one + updateFiles("ssl/reload/node-new-ca.crt.pem", pemCertFilePath); + updateFiles("ssl/reload/node-new-ca.key.pem", pemKeyFilePath); + + RestHelper.HttpResponse reloadCertsResponse = rh.executePutRequest(RELOAD_TRANSPORT_CERTS_ENDPOINT, null); + + assertThat(reloadCertsResponse.getStatusCode(), is(500)); + assertThat( + DefaultObjectMapper.readTree(reloadCertsResponse.getBody()).get("error").get("root_cause").get(0).get("reason").asText(), + is( + "OpenSearchSecurityException[Error while initializing transport SSL layer from PEM: java.lang.Exception: " + + "New Certs do not have valid Issuer DN, Subject DN or SAN.]; nested: Exception[New Certs do not have valid Issuer DN, Subject DN or SAN.];" + ) + ); + } + + @Test + public void testReloadTransportCertDifferentTrustChain_defaultSettingValidationFail() throws Exception { + updateFiles(defaultCertFilePath, pemCertFilePath); + updateFiles(defaultKeyFilePath, pemKeyFilePath); + initTestCluster(pemCertFilePath, pemKeyFilePath, pemCertFilePath, pemKeyFilePath, true, null, null); + + RestHelper rh = getRestHelperAdminUser(); + // Change transport certs to one signed by a different CA than the previous one + updateFiles("ssl/reload/node-new-ca.crt.pem", pemCertFilePath); + updateFiles("ssl/reload/node-new-ca.key.pem", pemKeyFilePath); + + RestHelper.HttpResponse reloadCertsResponse = rh.executePutRequest(RELOAD_TRANSPORT_CERTS_ENDPOINT, null); + + assertThat(reloadCertsResponse.getStatusCode(), is(500)); + assertThat( + DefaultObjectMapper.readTree(reloadCertsResponse.getBody()).get("error").get("root_cause").get(0).get("reason").asText(), + is( + "OpenSearchSecurityException[Error while initializing transport SSL layer from PEM: java.lang.Exception: " + + "New Certs do not have valid Issuer DN, Subject DN or SAN.]; nested: Exception[New Certs do not have valid Issuer DN, Subject DN or SAN.];" + ) + ); + } + /** * * @param rh RestHelper to perform rest actions on the cluster @@ -214,20 +368,18 @@ private void updateFiles(String srcFile, String dstFile) { FileHelper.copyFileContents(FileHelper.getAbsoluteFilePathFromClassPath(srcFile).toString(), dstFile); } - private JsonNode getUpdatedCertDetailsExpectedResponse(String updateChannel) { - String updateKey = (Objects.equals(updateChannel, "http")) ? HTTP_CERTIFICATES_LIST_KEY : TRANSPORT_CERTIFICATES_LIST_KEY; - String oldKey = (Objects.equals(updateChannel, "http")) ? TRANSPORT_CERTIFICATES_LIST_KEY : HTTP_CERTIFICATES_LIST_KEY; + private JsonNode getCertDetailsExpectedResponse( + List> httpCertDetails, + List> transportCertDetails + ) { final var updatedCertDetailsResponse = DefaultObjectMapper.objectMapper.createObjectNode(); - updatedCertDetailsResponse.set(updateKey, buildCertsInfoNode(NEW_NODE_CERT_DETAILS)); - updatedCertDetailsResponse.set(oldKey, buildCertsInfoNode(NODE_CERT_DETAILS)); + updatedCertDetailsResponse.set("http_certificates_list", buildCertsInfoNode(httpCertDetails)); + updatedCertDetailsResponse.set("transport_certificates_list", buildCertsInfoNode(transportCertDetails)); return updatedCertDetailsResponse; } private JsonNode getInitCertDetailsExpectedResponse() { - final var initCertDetailsResponse = DefaultObjectMapper.objectMapper.createObjectNode(); - initCertDetailsResponse.set(HTTP_CERTIFICATES_LIST_KEY, buildCertsInfoNode(NODE_CERT_DETAILS)); - initCertDetailsResponse.set(TRANSPORT_CERTIFICATES_LIST_KEY, buildCertsInfoNode(NODE_CERT_DETAILS)); - return initCertDetailsResponse; + return getCertDetailsExpectedResponse(INITIAL_NODE_CERT_DETAILS, INITIAL_NODE_CERT_DETAILS); } private JsonNode buildCertsInfoNode(final List> certsInfo) { @@ -273,25 +425,29 @@ private RestHelper getRestHelperNonAdminUser() { private void initClusterWithTestCerts() throws Exception { updateFiles(defaultCertFilePath, pemCertFilePath); updateFiles(defaultKeyFilePath, pemKeyFilePath); - initTestCluster(pemCertFilePath, pemKeyFilePath, pemCertFilePath, pemKeyFilePath, true); + initTestCluster(pemCertFilePath, pemKeyFilePath, pemCertFilePath, pemKeyFilePath, true, true, true); } /** * Helper method to initialize test cluster for SSL Certificate Reload Tests - * @param transportPemCertFilePath Absolute Path to transport pem cert file - * @param transportPemKeyFilePath Absolute Path to transport pem key file - * @param httpPemCertFilePath Absolute Path to transport pem cert file - * @param httpPemKeyFilePath Absolute Path to transport pem key file - * @param sslCertReload Sets the ssl cert reload flag + * @param transportPemCertFilePath Absolute Path to transport pem cert file + * @param transportPemKeyFilePath Absolute Path to transport pem key file + * @param httpPemCertFilePath Absolute Path to transport pem cert file + * @param httpPemKeyFilePath Absolute Path to transport pem key file + * @param sslCertReload Sets the ssl cert reload flag + * @param httpEnforceReloadDnVerification Sets the http enforce reload dn verification flag if non-null + * @param transportEnforceReloadDnVerification Sets the transport enforce reload dn verification flag if non-null */ private void initTestCluster( final String transportPemCertFilePath, final String transportPemKeyFilePath, final String httpPemCertFilePath, final String httpPemKeyFilePath, - final boolean sslCertReload + final boolean sslCertReload, + final Boolean httpEnforceReloadDnVerification, + final Boolean transportEnforceReloadDnVerification ) throws Exception { - final Settings settings = Settings.builder() + final Settings.Builder settingsBuilder = Settings.builder() .putList(ConfigConstants.SECURITY_AUTHCZ_ADMIN_DN, "CN=kirk,OU=client,O=client,L=Test,C=DE") .putList(ConfigConstants.SECURITY_NODES_DN, "CN=node-1.example.com,OU=SSL,O=Test,L=Test,C=DE") .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENABLED, true) @@ -310,8 +466,17 @@ private void initTestCluster( SSLConfigConstants.SECURITY_SSL_HTTP_PEMTRUSTEDCAS_FILEPATH, FileHelper.getAbsoluteFilePathFromClassPath("ssl/reload/root-ca.pem") ) - .put(ConfigConstants.SECURITY_SSL_CERT_RELOAD_ENABLED, sslCertReload) - .build(); + .put(ConfigConstants.SECURITY_SSL_CERT_RELOAD_ENABLED, sslCertReload); + + if (httpEnforceReloadDnVerification != null) settingsBuilder.put( + SSLConfigConstants.SECURITY_SSL_HTTP_ENFORCE_CERT_RELOAD_DN_VERIFICATION, + httpEnforceReloadDnVerification + ); + + if (transportEnforceReloadDnVerification != null) settingsBuilder.put( + SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENFORCE_CERT_RELOAD_DN_VERIFICATION, + transportEnforceReloadDnVerification + ); final Settings initTransportClientSettings = Settings.builder() .put( @@ -325,7 +490,7 @@ private void initTestCluster( ) .build(); - setup(initTransportClientSettings, new DynamicSecurityConfig(), settings, true, clusterConfiguration); + setup(initTransportClientSettings, new DynamicSecurityConfig(), settingsBuilder.build(), true, clusterConfiguration); } } diff --git a/src/test/resources/ssl/reload/README.txt b/src/test/resources/ssl/reload/README.txt new file mode 100644 index 0000000000..a149c42284 --- /dev/null +++ b/src/test/resources/ssl/reload/README.txt @@ -0,0 +1,29 @@ +Commands to generate node-new-ca.crt.pem, node-new-ca.key.pem, secondary-root-ca.pem, secondary-signing-ca.pem: + +# generate new secondary root CA +openssl genrsa -out secondary-root-ca-key.pem 2048 +openssl req -new -x509 -sha256 -days 3650 -key secondary-root-ca-key.pem -subj "/DC=com/DC=example/O=Example Com Inc./OU=Example Com Inc. Secondary Root CA/CN=Example Com Inc. Secondary Root CA" -addext "basicConstraints = critical,CA:TRUE" -addext "keyUsage = critical, digitalSignature, keyCertSign, cRLSign" -addext "subjectKeyIdentifier = hash" -addext "authorityKeyIdentifier = keyid:always,issuer:always" -out secondary-root-ca.pem + +# generate new secondary signing CA, signed by the new secondary root CA + +openssl genrsa -out secondary-signing-ca-key-temp.pem 2048 +openssl pkcs8 -inform PEM -outform PEM -in secondary-signing-ca-key-temp.pem -topk8 -nocrypt -v1 PBE-SHA1-3DES -out secondary-signing-ca-key.pem +openssl req -new -key secondary-signing-ca-key.pem -subj "/DC=com/DC=example/O=Example Com Inc./OU=Example Com Inc. Secondary Signing CA/CN=Example Com Inc. Secondary Signing CA" -out secondary-signing-ca-key.csr +printf "basicConstraints = critical,CA:TRUE" > secondary-signing-ca_ext.conf +printf "basicConstraints = critical,CA:TRUE\nkeyUsage = critical, digitalSignature, keyCertSign, cRLSign\nsubjectKeyIdentifier = hash\nauthorityKeyIdentifier = keyid:always,issuer:always" > secondary-signing-ca_ext.conf +openssl x509 -req -in secondary-signing-ca-key.csr -out secondary-signing-ca.pem -CA secondary-root-ca.pem -CAkey secondary-root-ca-key.pem -CAcreateserial -days 3650 -extfile secondary-signing-ca_ext.conf + +# generate a new node cert, signed by the new secondary signing key CA +openssl genrsa -out node-new-ca-key-temp.pem 2048 +openssl pkcs8 -inform PEM -outform PEM -in node-new-ca-key-temp.pem -topk8 -nocrypt -v1 PBE-SHA1-3DES -out node-new-ca.key.pem +openssl req -new -key node-new-ca.key.pem -subj "/C=DE/L=Test/O=Test/OU=SSL/CN=node-1.example.com" -out node-new-ca.csr +printf "subjectAltName = RID:1.2.3.4.5.5, DNS:node-1.example.com, DNS:localhost, IP:127.0.0.1" > node-new-ca_ext.conf +openssl x509 -req -in node-new-ca.csr -out node-new-ca.pem -CA secondary-signing-ca.pem -CAkey secondary-signing-ca-key.pem -CAcreateserial -days 3650 -extfile node-new-ca_ext.conf + +cat node-new-ca.pem > node-new-ca.crt.pem +cat secondary-signing-ca.pem >> node-new-ca.crt.pem +cat secondary-root-ca.pem >> node-new-ca.crt.pem + +# for tests to pass, the new secondary-signing-ca.pem and secondary-root-ca.pem keys should also be added to the truststore.jks file, e.g.: +keytool -import -alias secondary-root-ca -file secondary-root-ca.pem -storetype JKS -keystore truststore.jks +keytool -import -alias secondary-signing-ca -file secondary-signing-ca.pem -storetype JKS -keystore truststore.jks diff --git a/src/test/resources/ssl/reload/node-new-ca.crt.pem b/src/test/resources/ssl/reload/node-new-ca.crt.pem new file mode 100644 index 0000000000..2bf9284f09 --- /dev/null +++ b/src/test/resources/ssl/reload/node-new-ca.crt.pem @@ -0,0 +1,82 @@ +-----BEGIN CERTIFICATE----- +MIIEBzCCAu+gAwIBAgIUUN4lYU0yobNFo1xcluReeadmlaUwDQYJKoZIhvcNAQEL +BQAwgakxEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt +cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMS4wLAYDVQQLDCVFeGFtcGxl +IENvbSBJbmMuIFNlY29uZGFyeSBTaWduaW5nIENBMS4wLAYDVQQDDCVFeGFtcGxl +IENvbSBJbmMuIFNlY29uZGFyeSBTaWduaW5nIENBMB4XDTI0MDkxNzAwMTU0OFoX +DTM0MDkxNTAwMTU0OFowVjELMAkGA1UEBhMCREUxDTALBgNVBAcMBFRlc3QxDTAL +BgNVBAoMBFRlc3QxDDAKBgNVBAsMA1NTTDEbMBkGA1UEAwwSbm9kZS0xLmV4YW1w +bGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtJG372aVfTYZ +tQ6udEQzC9RNy8+SqBZEproPBdYupOZ2l0tKgGykoAI0iX/p3gzQlYBSmSVduKLZ +n5E/nQCb+Rqbi1uoZrojEQxq538RXWmI9X72MyKFnqcgjZW9qCBn0ok5J0fSp7kS +55I6IzJhrJFqJKdn/i1dTReyg0tjSa/dR2yHbFj97gdXAnnte7xa87ounKZoFtme +rhhfVfbnkxQfSFecg0AltBiuhB9TxovRTo1TOVPpAUhBFBaj4ILSyGJdG9qQ11OM +L+QUd6TjQB0qFSVaf/BGu/0Umz1lp1OrrQkouaTQfuQ+3tOY3hwCM4PdL03YbBYX +r/H7EmhJ2QIDAQABo3kwdzA1BgNVHREELjAsiAUqAwQFBYISbm9kZS0xLmV4YW1w +bGUuY29tgglsb2NhbGhvc3SHBH8AAAEwHQYDVR0OBBYEFCBH9UyAoNd2nq/4PHuP +6XqCvcMnMB8GA1UdIwQYMBaAFFLGvw6mimoIfjgzjHjhUfVNAeAIMA0GCSqGSIb3 +DQEBCwUAA4IBAQAOhTfnE+uTD0PQy+/HT2uQKRMsn+f6CeiHTgTWkA7+XXECXyBI +B8cGnXEqNRg7gInrnYpsNv19Q5v4fghMG+5mTO0iDhSCL3ttXVy3J7yvb9IWgc12 +34YC7BeTe8DB+vATTnxEibOqXX8YhB/n9pB/xoqs7XUTVTP56QYcMZZvjzdIJhp9 +kpydel7TIDqJmG7HPkjVn0caxdsGFaBF5XmI4o73xlJVEZrN5OMy9yao6kXrNiqD +GPRg6y3KTtrGXNImTs9+iJhLfBtT3i8/UU7T8vC9yfU6JDC6CWDRIwHNtIBY2Yp+ +cLMxoh/SZHFqLPguzH2RWmwa7mgEOet1RYVe +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFCDCCA/CgAwIBAgIUfUpmQ/BPCGTsPLW7rrPbkEU1RcwwDQYJKoZIhvcNAQEL +BQAwgaMxEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt +cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSswKQYDVQQLDCJFeGFtcGxl +IENvbSBJbmMuIFNlY29uZGFyeSBSb290IENBMSswKQYDVQQDDCJFeGFtcGxlIENv +bSBJbmMuIFNlY29uZGFyeSBSb290IENBMB4XDTI0MDkxNzAwMTU0OFoXDTM0MDkx +NTAwMTU0OFowgakxEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZ +FgdleGFtcGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMS4wLAYDVQQLDCVF +eGFtcGxlIENvbSBJbmMuIFNlY29uZGFyeSBTaWduaW5nIENBMS4wLAYDVQQDDCVF +eGFtcGxlIENvbSBJbmMuIFNlY29uZGFyeSBTaWduaW5nIENBMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjUd34V1uf+OwGXIBce+4O/UX35yCxY0LHt48 +wNIGkEs3StbTG5/qjkeAIFr2EUpRX5c0n5sIWdJX1cV/drWrhUzy6Ya1jvQiTA+i +k4YVVkFsz9QajgP+UPS06ZLkFldBofd/Su4GW5YEBlOBfxbsr8+E+73M/8sU1/wD +QLwPZGrkN7Cc37qi0Sf3blCNsjwfZPrGm+J/4hxdlJKuimo3Ctfwtlv/cIJZv7aG +RPksgsiirrk//nrW24wCQjqernuRayT0+2KL7OIn7UH2XL4nUUKU4cHYJOeiTNz+ +ds/uP1FG5WAvQ7CEyh6z2aXxGhZ89ZquATFy2paLpqfWgARiiwIDAQABo4IBKjCC +ASYwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFFLG +vw6mimoIfjgzjHjhUfVNAeAIMIHjBgNVHSMEgdswgdiAFLmdh4hHmYd6TqjvfdgK +1x/HzhmeoYGppIGmMIGjMRMwEQYKCZImiZPyLGQBGRYDY29tMRcwFQYKCZImiZPy +LGQBGRYHZXhhbXBsZTEZMBcGA1UECgwQRXhhbXBsZSBDb20gSW5jLjErMCkGA1UE +CwwiRXhhbXBsZSBDb20gSW5jLiBTZWNvbmRhcnkgUm9vdCBDQTErMCkGA1UEAwwi +RXhhbXBsZSBDb20gSW5jLiBTZWNvbmRhcnkgUm9vdCBDQYIULRaaYWRGTgsg6K21 +3Aw9UyZJF+8wDQYJKoZIhvcNAQELBQADggEBAJo5QHvLgfH3VVJlNwFrGdNH1dCh +/mqPpqhjHCG8OUl2H8+dFsu/WfY7k/tcrMHCJHSVMbBiPxKM1MlR2aSIrGW7SNVZ +mrk0QfBHvhKdXOnUcPjp6CL7BAwgrKT9h0/v5ky/GutAL0L7N1Enntw+WWdI0SAn +JIaCzEN4s3VniDSyULZ7J3E4z7wmeLhzHf1ugyEoPOehP1RZzVJDLExZ30dXDUlG +qUQaUkBAjclD4i5vybF+CGGhCzIi0UTb+VmHNfi3yqYwltYGJzELvYw3ce/cVkSm +B4Qqx0niiFQfguX6MduWB067IXDLKu51ovnA+h72FKd7iZSrKg+qCEy0eYA= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFAjCCA+qgAwIBAgIULRaaYWRGTgsg6K213Aw9UyZJF+8wDQYJKoZIhvcNAQEL +BQAwgaMxEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt +cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSswKQYDVQQLDCJFeGFtcGxl +IENvbSBJbmMuIFNlY29uZGFyeSBSb290IENBMSswKQYDVQQDDCJFeGFtcGxlIENv +bSBJbmMuIFNlY29uZGFyeSBSb290IENBMB4XDTI0MDkxNzAwMTU0OFoXDTM0MDkx +NTAwMTU0OFowgaMxEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZ +FgdleGFtcGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSswKQYDVQQLDCJF +eGFtcGxlIENvbSBJbmMuIFNlY29uZGFyeSBSb290IENBMSswKQYDVQQDDCJFeGFt +cGxlIENvbSBJbmMuIFNlY29uZGFyeSBSb290IENBMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAqRYm2F+Ejfg3Vqmld+dXzIThERJuzyqpq7FUNegfAebM +e7aelHiAMSecYroxj990HdCp1yDhryRxzdGHkPK7VHzH695th8N1su7wO37cspjX +ZxMexiZuwV1t/N8khi20MItqa6sYY4gkBLoGiT5DdJNTJHv3Ammx+PmYHIRF0S1P +P1j2nd+Kxaj1Il4sInUo7BqbmO794QdICgJQ5XFeXmEV+4uhoPSHEoOfAlWUTKA9 +a9rugrY0k3JlUTF0tIPLEWOcMxEcQj6uYFehxakwiOnZwgkJMCSbhsoEBzq+i7Eb +2Wob7d2Gn0De3Z+ZruVIJzY0MpHWrDUyny/Qi17nDwIDAQABo4IBKjCCASYwDwYD +VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFLmdh4hHmYd6 +TqjvfdgK1x/HzhmeMIHjBgNVHSMEgdswgdiAFLmdh4hHmYd6TqjvfdgK1x/Hzhme +oYGppIGmMIGjMRMwEQYKCZImiZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYH +ZXhhbXBsZTEZMBcGA1UECgwQRXhhbXBsZSBDb20gSW5jLjErMCkGA1UECwwiRXhh +bXBsZSBDb20gSW5jLiBTZWNvbmRhcnkgUm9vdCBDQTErMCkGA1UEAwwiRXhhbXBs +ZSBDb20gSW5jLiBTZWNvbmRhcnkgUm9vdCBDQYIULRaaYWRGTgsg6K213Aw9UyZJ +F+8wDQYJKoZIhvcNAQELBQADggEBAI6PJGGXH2fIlrZQFZXkuLqjMrR8K+/60cq0 +4qqjTg8p+vQyB66BJSh4BiUM2sh7SwGKpehNB8QQXEZoyzpWY+Cdcm4ty4F430xS +uz/uW0NObhnJnyURlHf1szHTr91/1yX7eCtpUA1X9cjtXYS/uR911BCotdgmp3N9 +lHp+DjMx3j/xsGAuC1B2vmuLaMXA8SeYziDx+9KUHidMM7v/JsDZwc8XKCK+i12s +yIAv7Tuk5drq3x7ZCA3k9Xja/YqpaPNSP6iVsdM57NLPfZA9ilNuSMD49No6q9wW +dJ7sJEGDdICEBTuL9bCnwv/PZQ8ohJMJ+7Ike8f6tz8TsH3C+fg= +-----END CERTIFICATE----- diff --git a/src/test/resources/ssl/reload/node-new-ca.key.pem b/src/test/resources/ssl/reload/node-new-ca.key.pem new file mode 100644 index 0000000000..1fa12aa5e5 --- /dev/null +++ b/src/test/resources/ssl/reload/node-new-ca.key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC0kbfvZpV9Nhm1 +Dq50RDML1E3Lz5KoFkSmug8F1i6k5naXS0qAbKSgAjSJf+neDNCVgFKZJV24otmf +kT+dAJv5GpuLW6hmuiMRDGrnfxFdaYj1fvYzIoWepyCNlb2oIGfSiTknR9KnuRLn +kjojMmGskWokp2f+LV1NF7KDS2NJr91HbIdsWP3uB1cCee17vFrzui6cpmgW2Z6u +GF9V9ueTFB9IV5yDQCW0GK6EH1PGi9FOjVM5U+kBSEEUFqPggtLIYl0b2pDXU4wv +5BR3pONAHSoVJVp/8Ea7/RSbPWWnU6utCSi5pNB+5D7e05jeHAIzg90vTdhsFhev +8fsSaEnZAgMBAAECggEABVlpxwxVcmOnaE86iNQ6ZOfRtC9+iz85omzRpB0fvZ/c +NIg0+U/+ooTeNJKXBY6AoWUvTT0npSAh7VG6vjZ16G/K2tqIxx5NiqRBCIGhrJBD +T+6GcaZcqgIOe1NLzo7DNJ//EvRUP8bCUhzpXwCPlzKpn9Nbx9JlOLLyhWQ22Uhx +NQVIOd3qvhAvU/LF03fMPAqxeHXD0KOQNNxNPqwTWW1rRi6bzvMud0icQhDjTVBf +gUhowZFdDnt3NIiMmh997Fnjbx2J5BTJ/tWnyG4pVO8d4JX5RDcZOx+MFBO9ypQ/ +FqhGu+J3xjMzSP+Y7kKHI60KBMCRnz2hEUP2IN+xxwKBgQDkiBtpJkRshaP27Bbw +xpuSvsKRAYdMDSlfReKQHEs3hJ0w3wFT0ofgMS49PyzHu0TyQ39jNAJ3YkdpRpIC +nI9fcDV4xS6G5kz72U4yamm1RF7TdMoU1WWMqmtm7i9Cdjrd7z/WGnBUH2pmenJ7 +IDoeALme+GxNwyakaSKaemOlGwKBgQDKRc/uln6AIxtlWBqgo8XGLyzReK0EYdnN +jwp7CcOuza//q9/P1fh/NA4rj0uiPD0SYX92DmaGRuuF/FwQ7OnAw9x81JlQeBbW +iSH3IzFSXP6kuY+SpUkRuWLXuNByIbsICOWN761PyIDV7TJAG/e6G/SLHUoOA6+G +aOQopG+gGwKBgDdmIzbvNuET2HaQLtN5YddF9QaP10uBWUkmOND0eutfc3eYZ8r5 +G0Umxu3D9cgJRqJv6F6VChAEvAjyOYz2hO7+1YeMTUYYaAsZV5JzJ2Lwywf5pM8+ +F9rsqRKPpNc4r/aC+/eb+yT5ZKKpBj2Ax3XkeRrnX+HN7/0lG2VVS/iHAoGAFAvN +KqkRimNwUJ/lq6vvas+8ElpyUy/bZQrbEAyMryNFYQJIoRFkmj6vdNOzvDVaHBs7 +hZixwnb+2n2DJk5EcE046cosE5SDNunKSvLa7X234t1dBDyLPE1yJUz0o4sCPS6c +iW+KbpDBa/Ig+8eJypEAsFTLxQ3KdHiqu/hn86cCgYBygyypD9R11k8X46ayXFcV +C+bpC4GsRliRvA0smFIAC75urzngLYyyP2ueonusE0cNYARlcbV3FcLDLuvEGoJL +enlYi4k9F+nCZaQ2ylWl2H6ud0/kCnIfjv+4Knf15Wz58VkH09AvbmqJNyd6kB0e +19JMEgOOugnWgfUNsBipfQ== +-----END PRIVATE KEY----- diff --git a/src/test/resources/ssl/reload/secondary-root-ca.pem b/src/test/resources/ssl/reload/secondary-root-ca.pem new file mode 100644 index 0000000000..81d8309898 --- /dev/null +++ b/src/test/resources/ssl/reload/secondary-root-ca.pem @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIFAjCCA+qgAwIBAgIULRaaYWRGTgsg6K213Aw9UyZJF+8wDQYJKoZIhvcNAQEL +BQAwgaMxEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt +cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSswKQYDVQQLDCJFeGFtcGxl +IENvbSBJbmMuIFNlY29uZGFyeSBSb290IENBMSswKQYDVQQDDCJFeGFtcGxlIENv +bSBJbmMuIFNlY29uZGFyeSBSb290IENBMB4XDTI0MDkxNzAwMTU0OFoXDTM0MDkx +NTAwMTU0OFowgaMxEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZ +FgdleGFtcGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSswKQYDVQQLDCJF +eGFtcGxlIENvbSBJbmMuIFNlY29uZGFyeSBSb290IENBMSswKQYDVQQDDCJFeGFt +cGxlIENvbSBJbmMuIFNlY29uZGFyeSBSb290IENBMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAqRYm2F+Ejfg3Vqmld+dXzIThERJuzyqpq7FUNegfAebM +e7aelHiAMSecYroxj990HdCp1yDhryRxzdGHkPK7VHzH695th8N1su7wO37cspjX +ZxMexiZuwV1t/N8khi20MItqa6sYY4gkBLoGiT5DdJNTJHv3Ammx+PmYHIRF0S1P +P1j2nd+Kxaj1Il4sInUo7BqbmO794QdICgJQ5XFeXmEV+4uhoPSHEoOfAlWUTKA9 +a9rugrY0k3JlUTF0tIPLEWOcMxEcQj6uYFehxakwiOnZwgkJMCSbhsoEBzq+i7Eb +2Wob7d2Gn0De3Z+ZruVIJzY0MpHWrDUyny/Qi17nDwIDAQABo4IBKjCCASYwDwYD +VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFLmdh4hHmYd6 +TqjvfdgK1x/HzhmeMIHjBgNVHSMEgdswgdiAFLmdh4hHmYd6TqjvfdgK1x/Hzhme +oYGppIGmMIGjMRMwEQYKCZImiZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYH +ZXhhbXBsZTEZMBcGA1UECgwQRXhhbXBsZSBDb20gSW5jLjErMCkGA1UECwwiRXhh +bXBsZSBDb20gSW5jLiBTZWNvbmRhcnkgUm9vdCBDQTErMCkGA1UEAwwiRXhhbXBs +ZSBDb20gSW5jLiBTZWNvbmRhcnkgUm9vdCBDQYIULRaaYWRGTgsg6K213Aw9UyZJ +F+8wDQYJKoZIhvcNAQELBQADggEBAI6PJGGXH2fIlrZQFZXkuLqjMrR8K+/60cq0 +4qqjTg8p+vQyB66BJSh4BiUM2sh7SwGKpehNB8QQXEZoyzpWY+Cdcm4ty4F430xS +uz/uW0NObhnJnyURlHf1szHTr91/1yX7eCtpUA1X9cjtXYS/uR911BCotdgmp3N9 +lHp+DjMx3j/xsGAuC1B2vmuLaMXA8SeYziDx+9KUHidMM7v/JsDZwc8XKCK+i12s +yIAv7Tuk5drq3x7ZCA3k9Xja/YqpaPNSP6iVsdM57NLPfZA9ilNuSMD49No6q9wW +dJ7sJEGDdICEBTuL9bCnwv/PZQ8ohJMJ+7Ike8f6tz8TsH3C+fg= +-----END CERTIFICATE----- diff --git a/src/test/resources/ssl/reload/secondary-signing-ca.pem b/src/test/resources/ssl/reload/secondary-signing-ca.pem new file mode 100644 index 0000000000..53f989d0b3 --- /dev/null +++ b/src/test/resources/ssl/reload/secondary-signing-ca.pem @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIFCDCCA/CgAwIBAgIUfUpmQ/BPCGTsPLW7rrPbkEU1RcwwDQYJKoZIhvcNAQEL +BQAwgaMxEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt +cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSswKQYDVQQLDCJFeGFtcGxl +IENvbSBJbmMuIFNlY29uZGFyeSBSb290IENBMSswKQYDVQQDDCJFeGFtcGxlIENv +bSBJbmMuIFNlY29uZGFyeSBSb290IENBMB4XDTI0MDkxNzAwMTU0OFoXDTM0MDkx +NTAwMTU0OFowgakxEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZ +FgdleGFtcGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMS4wLAYDVQQLDCVF +eGFtcGxlIENvbSBJbmMuIFNlY29uZGFyeSBTaWduaW5nIENBMS4wLAYDVQQDDCVF +eGFtcGxlIENvbSBJbmMuIFNlY29uZGFyeSBTaWduaW5nIENBMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjUd34V1uf+OwGXIBce+4O/UX35yCxY0LHt48 +wNIGkEs3StbTG5/qjkeAIFr2EUpRX5c0n5sIWdJX1cV/drWrhUzy6Ya1jvQiTA+i +k4YVVkFsz9QajgP+UPS06ZLkFldBofd/Su4GW5YEBlOBfxbsr8+E+73M/8sU1/wD +QLwPZGrkN7Cc37qi0Sf3blCNsjwfZPrGm+J/4hxdlJKuimo3Ctfwtlv/cIJZv7aG +RPksgsiirrk//nrW24wCQjqernuRayT0+2KL7OIn7UH2XL4nUUKU4cHYJOeiTNz+ +ds/uP1FG5WAvQ7CEyh6z2aXxGhZ89ZquATFy2paLpqfWgARiiwIDAQABo4IBKjCC +ASYwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFFLG +vw6mimoIfjgzjHjhUfVNAeAIMIHjBgNVHSMEgdswgdiAFLmdh4hHmYd6TqjvfdgK +1x/HzhmeoYGppIGmMIGjMRMwEQYKCZImiZPyLGQBGRYDY29tMRcwFQYKCZImiZPy +LGQBGRYHZXhhbXBsZTEZMBcGA1UECgwQRXhhbXBsZSBDb20gSW5jLjErMCkGA1UE +CwwiRXhhbXBsZSBDb20gSW5jLiBTZWNvbmRhcnkgUm9vdCBDQTErMCkGA1UEAwwi +RXhhbXBsZSBDb20gSW5jLiBTZWNvbmRhcnkgUm9vdCBDQYIULRaaYWRGTgsg6K21 +3Aw9UyZJF+8wDQYJKoZIhvcNAQELBQADggEBAJo5QHvLgfH3VVJlNwFrGdNH1dCh +/mqPpqhjHCG8OUl2H8+dFsu/WfY7k/tcrMHCJHSVMbBiPxKM1MlR2aSIrGW7SNVZ +mrk0QfBHvhKdXOnUcPjp6CL7BAwgrKT9h0/v5ky/GutAL0L7N1Enntw+WWdI0SAn +JIaCzEN4s3VniDSyULZ7J3E4z7wmeLhzHf1ugyEoPOehP1RZzVJDLExZ30dXDUlG +qUQaUkBAjclD4i5vybF+CGGhCzIi0UTb+VmHNfi3yqYwltYGJzELvYw3ce/cVkSm +B4Qqx0niiFQfguX6MduWB067IXDLKu51ovnA+h72FKd7iZSrKg+qCEy0eYA= +-----END CERTIFICATE----- diff --git a/src/test/resources/ssl/reload/truststore.jks b/src/test/resources/ssl/reload/truststore.jks index c750f9807aa22339c19d05e510990a57ceef4758..217c4d09b4f5b162bf6ede35fb2fd19594d7643b 100644 GIT binary patch delta 4196 zcmV-q5S#Dz3Wgy-FoF<*0s#Xsf)FeQ2`Yw2hW8Bt2LYgh5FG@95E(Fn5EU?j5D^9m zDuzgg_YDCD2B3lv0x*IP{sI92FoF;Fkw7aH3y2teV%KE-MHD{guQeLr8q(9yk$)tA zFM<4bb*1t?DM&>|2%CerL^6{oqhGZlovK>+EUsZblPP^hD~L%q zxW~7Pddh0Quz8MtIjhRlcHKX!)?~_n9VJ04tbRUiid|(je>zqAnluKlCsI%Q{d3+9Z|8OGY3x7y zE<%>#Q(8y4VXvO3xvRq5;)Gs5s5vxA$NYR{X7piJUYrJl>2Cc8_;Y`3exC-y%I1fu z|NEzt!9|78QrN}*5I9V@zo`9xF>?D|Jw)0&wUcAKd~)7nqACu2VfqH|Qlk?ksH9rv*UjIeMYJ!$5891rewLTb^}}Hu$h0 zPAh=ST!yzDAX#n?{32YgS;v4MGNIIf>Vj^#dL!B6y+^!%2J#D}cC_B(eRMjy?I&d6 zQ#XARl>73#`fz&sK||&YdDz9(H!CH=%_W2#z!a7c=F56HHo{>$QC~GGj$iaMdHlsW zq`O;q4zH)#*rSu-oRiL$Wy#A4oFd%mrPW1m?d-Wym>G{@7;_kdcGorAAkif=MGJBGbYSSH$%cSDZ7oW200}*ovw67d9V`cS($YR2iLE%&gL>%oKJg#!cNpcVYle zlLvfIG|VSIadx&k+Kq?z67}{awDo+EaOsa?eBNKg6T-viRPWK>|4Hi+a;Yg?m+}J$=Fb^DN=&hCpJAnnCB?TEGC%gWEy-nOm-EO%uAZe$+^1?b zE`fp{%v5&q$%gyd2_457A=xVl@+ylDQb`!N3RjjT{S6}iVxQc-oNY)em5Pp0nlc?`;`TZyx&?>rr%QZw_H4}BJkMLEwq{J@o=7vTZb3ho!V&I}9gOG){N##Qc+ z%25D(;BXxH<9YB`$+5V&SMVskNkbp5f?NG zqh9mr7%6)TSL!WR-t#m>3=BwjgEby$Ig0iu7{qR$xJyM60oGycF#dSI9qVSj27@udR)=?(~`0@$pGegMv2Ow?3NU) zJ$aSmId9+@mCM0n7 zctJ!HpH6eG0>f;6ENJ@TF`9)h%uOQ>E&9vHVKamsG942JJ%T`$grWT~KJlB_4;8~Hen_?Bp*W(Eo`+JqajMOOrZ$4FO}459xo zIQny(s>$7dorWQ=|0GyXEx3+@tN$FI!=9rC@>H%mwt7Kg!S+Hy9T$r@K;c51wS&z} zibe4eBh&$i{Dl%E3CywYySs&vNSY5{A-Ar|t(+Slb(GM{CHtbuS;u*ShlO{)+z3T) z@aR7ZjG@leT+)MEy}dhWTy=n~xAls{>kaaW(X4HMhUWSh!PK3D__)v3DPR^PLZ_$m zCn9yZnEpoS4(xcBL;Sdt7%5Gxmwx2H2vU!|9(OD`;Bg8aqk$=2h2AV9@=2qirW`=o zS!qSbf_NcqscGAXzCLi~g8oZlXkyszsc7Hoz*)X&(oa!9EJxH`Em+EGfgSLqB6$63 zHwZg_uGcd}(^GsPeLrYC^SL-@gD(%g6*1p>UWfm~(rQfn3LYr>UI4%SSXXv~d0kpZ z&_Kk&Aj<(j#grr>%fm~cblR(U9wZax)d{<^;K-t)!!2f3h0Ff<;GiK+0W%G&+nz!C zWsk0i`jMm87>Q%b#5P^Cn@{KIiGytCug1=Q$+@X;sai!*QJO`Wbs}E}y}yvUhkpK; zAN%~N)SIFSg;tGHUB%I+(PIS)f$fPcNSQvT84MFe3~a_XT7^i48zg5$P)*vc%+&R= zf}6Ppzz6fRY`+pOjSBLSASTqG!b5)9{F=T}mv!~#v5_9?q6P+^Ph7m}RSKUyxhc$l z?}K!eD1B}9MkH>@jmYHp6Ucvoe^>64>kXNL=Ab~*zmSciqDuk45NwHOqpBT3P+WW+ z?+?pvP`gx-YXI|D*F23>;IEX8*ajn0|-pbN)#y9=d!=lE=2v`(1Zh9{;$(Qz`! zV1T;jS(7#KQRfG?<2{!`j4ZW{;F~&TB{RSr@}JtnV(Fhz*ujaIzDsP) z5JRm7`XP)5R@G^*t0Kr0omt;i5M0Gf{}oIn?=)H_YkS!2bH98s9EP#L3G+ch#guG4 z#CdcX(R31XC`=5qgf#z>ZT>5wAE@Pw^(g;|m z-88M6cJ5U)Bi~+>tGsIlg?xn6IU`U;ng0!KzNtfl_QDN`|6$*dLIsuAv{Nyw01(>n ziIV8_7bv{6)EkUku<>N$DlI;9z4y%-_&_3%2FBZh)UOvllP;gBjdA1Tm1q90w)k5e;_LW delta 1341 zcmV-D1;YAM1qCpI1px*L zDuzgg_YDCD2B3lj@-Tt~?g9Y-FoFcRae3egG^!`N|uiWS)NYr0K$2~!PvU^FxeAQr*)jTJ(#*e8yQ zdcgN_Ejb(F3;ReP4(W)YJ0@Andbbu39;Wvl3&}V3y>Ru8O*2*B5t|npn$-pwtvDDK z#nee&HaB;>dd6al&|l+*{rU^+1qrf22Pw@lw;tB<97!(Q)l*A`9evzu*HbU_zspJ|K6Grpsj3}c=pbJ8GXhugda6pwmX+^%yH(H;PEx#ae_nT6iteH|! zqP(-g3b;0PIVidgGdFRJ=MnhC&SmBa;DqpVHP4x}30bDu%Urtl`R2~uH7fCmKWmSh zH@qcMNG~GFvdzjgelvsYh)kJ((#z;r&_jR&+UL66E!L5NSR-J`JdK9Q|2dl>4AEWf z+69VFS)i4{?eB-Fy@-b6qf5$;5&iS88Rx=Mw6ac*hgaUJmLj&&G?Pb(s)6lrQ&wCX zb8o$m@gEHpW5-dC&DUAs3N#^}=^UZ^;6U&Ozi4`FAd|0qw$$vXIx*9K;C1)4sDW@4 zy^U)cDCczg=MYcVJ}nbB;+}UHnd^(>&HqByd8Cf@*+$)94c3cjRox1s{dILT==n1O z0yl?hu<8FDpV|`{T1;72`>#<8;BtCgncM>(J9mfRTwCl|pgx~{b&<=pRWbqcfibzk~3_+%DH#Ji1rLu6+N8Vk)0mm(;fbkb+V!5m`MJmV2d zu}&Ja%Dm^rgxZmXPj4qm_p=fC}7s;{??PX(k%9YpYSGAYgyel5=)xnW(zZa#~j zxFIn7)h150{p+gR<4fM1ta?K42VI3$_)lkIH1>Vitduu=o;V71SM^bK557wGkDZ6B zX@bUa_C2rs2FiSYio$o`Iw16uS;MC!Bfu-FGSSaJ1>2EmegJ#9T~L>tX&$Sa-6jM* zenjwtLnq|l7Xhg4J}X5p#*xWy9t~14JkZE)L5mKL4iJt#U;iVJMR9}D!D%(KO(s6P z-JNYM?L;b;*8{l%3cRo4y+}GFzyCj*-R}$LC9Q#PlS~tTX-j~+08esmOkFCVjf4j= z$;XoCTg!V`wl>gsIZXR&{m@;8n7JN3#=M=OsZP4%+grOum!AXs%2`Q)O-1klK+-XH zJ!T}tXb|IW_)Op)cW5GX@0W7C?UCy7+=C7GFKI~B%I!;A#jrdQb>PoIz1nyy_AQ1^ z?N{(iLBLF0rv!Yw^DQ?TKziP&yD&{KF)$4V31Egu0c8UO0s#d81R!5j#|R$F6Yf5- zUZVTP!BjaL^Nd