>From Michael Blow <[email protected]>: Michael Blow has submitted this change. ( https://asterix-gerrit.ics.uci.edu/c/asterixdb/+/21028?usp=email )
Change subject: [ASTERIXDB-3744][*DB][STO] Support explicit blob storage certificates ...................................................................... [ASTERIXDB-3744][*DB][STO] Support explicit blob storage certificates Generated-by: Claude Opus 4.6 (via GitHub Copilot Agent, partial) Ext-ref: MB-68233 Change-Id: I0af4a59ff219de1a5213d90719a2995f8ebfe7b3 Reviewed-on: https://asterix-gerrit.ics.uci.edu/c/asterixdb/+/21028 Integration-Tests: Jenkins <[email protected]> Reviewed-by: Michael Blow <[email protected]> Tested-by: Michael Blow <[email protected]> Reviewed-by: Ian Maxon <[email protected]> --- M asterixdb/asterix-app/src/test/java/org/apache/asterix/test/common/TestConstants.java M asterixdb/asterix-app/src/test/java/org/apache/asterix/test/common/TestExecutor.java M asterixdb/asterix-app/src/test/resources/runtimets/results/api/cluster_state_1/cluster_state_1.1.regexadm M asterixdb/asterix-app/src/test/resources/runtimets/results/api/cluster_state_1_full/cluster_state_1_full.1.regexadm M asterixdb/asterix-app/src/test/resources/runtimets/results/api/cluster_state_1_less/cluster_state_1_less.1.regexadm M asterixdb/asterix-cloud/src/main/java/org/apache/asterix/cloud/clients/aws/s3/S3ClientConfig.java M asterixdb/asterix-cloud/src/main/java/org/apache/asterix/cloud/clients/aws/s3/S3CloudClient.java M asterixdb/asterix-cloud/src/main/java/org/apache/asterix/cloud/clients/aws/s3/S3ParallelDownloader.java A asterixdb/asterix-cloud/src/main/java/org/apache/asterix/cloud/clients/aws/s3/S3TrustManagerProvider.java M asterixdb/asterix-cloud/src/test/java/org/apache/asterix/cloud/s3/LSMS3Test.java M asterixdb/asterix-common/src/main/java/org/apache/asterix/common/config/CloudProperties.java M asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/util/ExternalDataConstants.java M asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/util/aws/s3/S3Utils.java 13 files changed, 297 insertions(+), 18 deletions(-) Approvals: Ian Maxon: Looks good to me, approved Jenkins: Verified Michael Blow: Looks good to me, but someone else must approve; Verified diff --git a/asterixdb/asterix-app/src/test/java/org/apache/asterix/test/common/TestConstants.java b/asterixdb/asterix-app/src/test/java/org/apache/asterix/test/common/TestConstants.java index c0ddfdf..480420a 100644 --- a/asterixdb/asterix-app/src/test/java/org/apache/asterix/test/common/TestConstants.java +++ b/asterixdb/asterix-app/src/test/java/org/apache/asterix/test/common/TestConstants.java @@ -37,6 +37,9 @@ public static final String S3_REGION_DEFAULT = "us-west-2"; public static final String S3_SERVICE_ENDPOINT_PLACEHOLDER = "%serviceEndpoint%"; public static final String S3_SERVICE_ENDPOINT_DEFAULT = "http://127.0.0.1:8001"; + public static final String S3_API_CERTIFICATES_PLACEHOLDER = "%apiCertificates%"; + public static final String S3_DDL_CERTIFICATES_PLACEHOLDER = "%ddlCertificates%"; + public static final String S3_HTTP_CERTIFICATES_PLACEHOLDER = "%httpCertificates%"; public static final String S3_SERVICE_ENDPOINT_KEY = "s3mockEndpoint"; public static final String S3_TEMPLATE = "(\"accessKeyId\"=\"" + S3_ACCESS_KEY_ID_DEFAULT + "\"),\n" + "(\"secretAccessKey\"=\"" + S3_SECRET_ACCESS_KEY_DEFAULT + "\"),\n" + "(\"region\"=\"" diff --git a/asterixdb/asterix-app/src/test/java/org/apache/asterix/test/common/TestExecutor.java b/asterixdb/asterix-app/src/test/java/org/apache/asterix/test/common/TestExecutor.java index 81ad57a..425df54 100644 --- a/asterixdb/asterix-app/src/test/java/org/apache/asterix/test/common/TestExecutor.java +++ b/asterixdb/asterix-app/src/test/java/org/apache/asterix/test/common/TestExecutor.java @@ -2531,6 +2531,9 @@ str = str.replace(TestConstants.S3_SERVICE_ENDPOINT_PLACEHOLDER, getS3ServiceEndpointDefault()); str = str.replace(TestConstants.S3_ACCESS_KEY_ID_PLACEHOLDER, getS3AccessKeyIdDefault()); str = str.replace(TestConstants.S3_SECRET_ACCESS_KEY_PLACEHOLDER, getS3SecretAccessKeyDefault()); + str = str.replace(TestConstants.S3_API_CERTIFICATES_PLACEHOLDER, getS3ApiCertificatesDefault()); + str = str.replace(TestConstants.S3_DDL_CERTIFICATES_PLACEHOLDER, getS3DdlCertificatesDefault()); + str = str.replace(TestConstants.S3_HTTP_CERTIFICATES_PLACEHOLDER, getS3HttpCertificatesDefault()); return str; } @@ -2543,6 +2546,10 @@ return TestConstants.S3_SECRET_ACCESS_KEY_DEFAULT; } + protected String getS3CertificatesDefault() { + return null; + } + protected String getS3ServiceEndpointDefault() { return TestConstants.S3_SERVICE_ENDPOINT_DEFAULT; } @@ -2551,6 +2558,36 @@ return TestConstants.S3_REGION_DEFAULT; } + protected String getS3ApiCertificatesDefault() { + String certificates = getS3CertificatesDefault(); + return certificates == null || certificates.isBlank() ? "" + : "&certificates=" + URLEncoder.encode(writeAsJsonString(List.of(certificates)), UTF_8); + } + + protected String getS3DdlCertificatesDefault() { + String certificates = getS3CertificatesDefault(); + return certificates == null || certificates.isBlank() ? "" + : ",\n\"certificates\":" + writeAsJsonString(List.of(certificates)); + } + + protected String getS3HttpCertificatesDefault() { + String certificates = getS3CertificatesDefault(); + return certificates == null || certificates.isBlank() ? "" + : ", \\\"certificates\\\":" + escapeForHttpBodyString(writeAsJsonString(List.of(certificates))); + } + + private static String escapeForHttpBodyString(String value) { + return value.replace("\\", "\\\\").replace("\"", "\\\""); + } + + private static String writeAsJsonString(Object value) { + try { + return OBJECT_WRITER.writeValueAsString(value); + } catch (IOException e) { + throw new IllegalStateException("Failed to serialize S3 test value", e); + } + } + protected String setS3Template(String str) { return str.replace("%template%", TestConstants.S3_TEMPLATE); } diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/api/cluster_state_1/cluster_state_1.1.regexadm b/asterixdb/asterix-app/src/test/resources/runtimets/results/api/cluster_state_1/cluster_state_1.1.regexadm index 6d7deed..ca1d858 100644 --- a/asterixdb/asterix-app/src/test/resources/runtimets/results/api/cluster_state_1/cluster_state_1.1.regexadm +++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/api/cluster_state_1/cluster_state_1.1.regexadm @@ -27,6 +27,7 @@ "cloud.storage.bucket" : "", "cloud.storage.cache.policy" : "selective", "cloud.storage.debug.mode.enabled" : false, + "cloud.storage.certificates" : [ ], "cloud.storage.debug.sweep.threshold.size" : 1073741824, "cloud.storage.disable.ssl.verify" : false, "cloud.storage.disk.monitor.interval" : 120, diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/api/cluster_state_1_full/cluster_state_1_full.1.regexadm b/asterixdb/asterix-app/src/test/resources/runtimets/results/api/cluster_state_1_full/cluster_state_1_full.1.regexadm index 08922ef..51c0437 100644 --- a/asterixdb/asterix-app/src/test/resources/runtimets/results/api/cluster_state_1_full/cluster_state_1_full.1.regexadm +++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/api/cluster_state_1_full/cluster_state_1_full.1.regexadm @@ -27,6 +27,7 @@ "cloud.storage.bucket" : "", "cloud.storage.cache.policy" : "selective", "cloud.storage.debug.mode.enabled" : false, + "cloud.storage.certificates" : [ ], "cloud.storage.debug.sweep.threshold.size" : 1073741824, "cloud.storage.disable.ssl.verify" : false, "cloud.storage.disk.monitor.interval" : 120, diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/api/cluster_state_1_less/cluster_state_1_less.1.regexadm b/asterixdb/asterix-app/src/test/resources/runtimets/results/api/cluster_state_1_less/cluster_state_1_less.1.regexadm index ecc29f0..35bb0e9 100644 --- a/asterixdb/asterix-app/src/test/resources/runtimets/results/api/cluster_state_1_less/cluster_state_1_less.1.regexadm +++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/api/cluster_state_1_less/cluster_state_1_less.1.regexadm @@ -27,6 +27,7 @@ "cloud.storage.bucket" : "", "cloud.storage.cache.policy" : "selective", "cloud.storage.debug.mode.enabled" : false, + "cloud.storage.certificates" : [ ], "cloud.storage.debug.sweep.threshold.size" : 1073741824, "cloud.storage.disable.ssl.verify" : false, "cloud.storage.disk.monitor.interval" : 120, diff --git a/asterixdb/asterix-cloud/src/main/java/org/apache/asterix/cloud/clients/aws/s3/S3ClientConfig.java b/asterixdb/asterix-cloud/src/main/java/org/apache/asterix/cloud/clients/aws/s3/S3ClientConfig.java index c9046a7..4ff5ed0 100644 --- a/asterixdb/asterix-cloud/src/main/java/org/apache/asterix/cloud/clients/aws/s3/S3ClientConfig.java +++ b/asterixdb/asterix-cloud/src/main/java/org/apache/asterix/cloud/clients/aws/s3/S3ClientConfig.java @@ -18,6 +18,8 @@ */ package org.apache.asterix.cloud.clients.aws.s3; +import java.util.Collection; +import java.util.Collections; import java.util.Map; import java.util.Objects; @@ -36,6 +38,7 @@ private final String endpoint; private final String prefix; private final boolean anonymousAuth; + private final Collection<String> certificates; private final long profilerLogInterval; private final int writeBufferSize; private final long tokenAcquireTimeout; @@ -52,22 +55,23 @@ private final boolean roundRobinDnsResolver; public S3ClientConfig(String region, String endpoint, String prefix, boolean anonymousAuth, - long profilerLogInterval, int writeBufferSize, S3ParallelDownloaderClientType parallelDownloaderClientType, - boolean roundRobinDnsResolver) { - this(region, endpoint, prefix, anonymousAuth, profilerLogInterval, writeBufferSize, 1, 0, 0, 0, false, false, - false, 0, 0, -1, parallelDownloaderClientType, roundRobinDnsResolver); + Collection<String> certificates, long profilerLogInterval, int writeBufferSize, + S3ParallelDownloaderClientType parallelDownloaderClientType, boolean roundRobinDnsResolver) { + this(region, endpoint, prefix, anonymousAuth, certificates, profilerLogInterval, writeBufferSize, 1, 0, 0, 0, + false, false, false, 0, 0, -1, parallelDownloaderClientType, roundRobinDnsResolver); } private S3ClientConfig(String region, String endpoint, String prefix, boolean anonymousAuth, - long profilerLogInterval, int writeBufferSize, long tokenAcquireTimeout, int writeMaxRequestsPerSeconds, - int readMaxRequestsPerSeconds, int requestsMaxHttpConnections, boolean forcePathStyle, - boolean disableSslVerify, boolean storageListEventuallyConsistent, int requestsMaxPendingHttpConnections, - int requestsHttpConnectionAcquireTimeout, int s3ReadTimeoutInSeconds, + Collection<String> certificates, long profilerLogInterval, int writeBufferSize, long tokenAcquireTimeout, + int writeMaxRequestsPerSeconds, int readMaxRequestsPerSeconds, int requestsMaxHttpConnections, + boolean forcePathStyle, boolean disableSslVerify, boolean storageListEventuallyConsistent, + int requestsMaxPendingHttpConnections, int requestsHttpConnectionAcquireTimeout, int s3ReadTimeoutInSeconds, S3ParallelDownloaderClientType parallelDownloaderClientType, boolean roundRobinDnsResolver) { this.region = Objects.requireNonNull(region, "region"); this.endpoint = endpoint; this.prefix = Objects.requireNonNull(prefix, "prefix"); this.anonymousAuth = anonymousAuth; + this.certificates = Objects.requireNonNull(certificates, "certificates"); this.profilerLogInterval = profilerLogInterval; this.writeBufferSize = writeBufferSize; this.tokenAcquireTimeout = tokenAcquireTimeout; @@ -87,11 +91,11 @@ public static S3ClientConfig of(CloudProperties cloudProperties) { return new S3ClientConfig(cloudProperties.getStorageRegion(), cloudProperties.getStorageEndpoint(), cloudProperties.getStoragePrefix(), cloudProperties.isStorageAnonymousAuth(), - cloudProperties.getProfilerLogInterval(), cloudProperties.getWriteBufferSize(), - cloudProperties.getTokenAcquireTimeout(), cloudProperties.getWriteMaxRequestsPerSecond(), - cloudProperties.getReadMaxRequestsPerSecond(), cloudProperties.getRequestsMaxHttpConnections(), - cloudProperties.isStorageForcePathStyle(), cloudProperties.isStorageDisableSSLVerify(), - cloudProperties.isStorageListEventuallyConsistent(), + cloudProperties.getStorageCertificates(), cloudProperties.getProfilerLogInterval(), + cloudProperties.getWriteBufferSize(), cloudProperties.getTokenAcquireTimeout(), + cloudProperties.getWriteMaxRequestsPerSecond(), cloudProperties.getReadMaxRequestsPerSecond(), + cloudProperties.getRequestsMaxHttpConnections(), cloudProperties.isStorageForcePathStyle(), + cloudProperties.isStorageDisableSSLVerify(), cloudProperties.isStorageListEventuallyConsistent(), cloudProperties.getRequestsMaxPendingHttpConnections(), cloudProperties.getRequestsHttpConnectionAcquireTimeout(), cloudProperties.getS3ReadTimeoutInSeconds(), S3ParallelDownloaderClientType.valueOf(cloudProperties.getS3ParallelDownloaderClientType()), @@ -125,10 +129,11 @@ // Dummy values; String region = ""; String prefix = ""; + Collection<String> certificates = Collections.emptyList(); boolean anonymousAuth = false; - return new S3ClientConfig(region, endPoint, prefix, anonymousAuth, profilerLogInterval, writeBufferSize, - S3ParallelDownloaderClientType.ASYNC, false); + return new S3ClientConfig(region, endPoint, prefix, anonymousAuth, certificates, profilerLogInterval, + writeBufferSize, S3ParallelDownloaderClientType.ASYNC, false); } public String getRegion() { @@ -188,6 +193,10 @@ return disableSslVerify; } + public Collection<String> getCertificates() { + return certificates; + } + public boolean isForcePathStyle() { return forcePathStyle; } diff --git a/asterixdb/asterix-cloud/src/main/java/org/apache/asterix/cloud/clients/aws/s3/S3CloudClient.java b/asterixdb/asterix-cloud/src/main/java/org/apache/asterix/cloud/clients/aws/s3/S3CloudClient.java index ab27ad4..9c98651 100644 --- a/asterixdb/asterix-cloud/src/main/java/org/apache/asterix/cloud/clients/aws/s3/S3CloudClient.java +++ b/asterixdb/asterix-cloud/src/main/java/org/apache/asterix/cloud/clients/aws/s3/S3CloudClient.java @@ -396,10 +396,13 @@ if (config.getEndpoint() != null && !config.getEndpoint().isEmpty()) { builder.endpointOverride(URI.create(config.getEndpoint())); } + ApacheHttpClient.Builder apacheBuilder = ApacheHttpClient.builder(); if (config.isDisableSslVerify()) { customHttpConfigBuilder.put(SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES, true); + } else if (!config.getCertificates().isEmpty()) { + apacheBuilder.tlsTrustManagersProvider(S3TrustManagerProvider.create(config.getCertificates())); } - SdkHttpClient httpClient = ApacheHttpClient.builder().buildWithDefaults(customHttpConfigBuilder.build()); + SdkHttpClient httpClient = apacheBuilder.buildWithDefaults(customHttpConfigBuilder.build()); builder.httpClient(httpClient); awsClients.setConsumingClient(builder.build()); diff --git a/asterixdb/asterix-cloud/src/main/java/org/apache/asterix/cloud/clients/aws/s3/S3ParallelDownloader.java b/asterixdb/asterix-cloud/src/main/java/org/apache/asterix/cloud/clients/aws/s3/S3ParallelDownloader.java index c1a92a6..a64e07b 100644 --- a/asterixdb/asterix-cloud/src/main/java/org/apache/asterix/cloud/clients/aws/s3/S3ParallelDownloader.java +++ b/asterixdb/asterix-cloud/src/main/java/org/apache/asterix/cloud/clients/aws/s3/S3ParallelDownloader.java @@ -49,6 +49,7 @@ import software.amazon.awssdk.services.s3.S3AsyncClient; import software.amazon.awssdk.services.s3.S3AsyncClientBuilder; import software.amazon.awssdk.services.s3.S3CrtAsyncClientBuilder; +import software.amazon.awssdk.services.s3.crt.S3CrtHttpConfiguration; import software.amazon.awssdk.services.s3.model.GetObjectRequest; import software.amazon.awssdk.transfer.s3.S3TransferManager; import software.amazon.awssdk.transfer.s3.model.CompletedDirectoryDownload; @@ -225,6 +226,10 @@ NettyNioAsyncHttpClient.Builder nettyBuilder = NettyNioAsyncHttpClient.builder().eventLoopGroup(SHARED_EVENT_LOOP); + if (!config.isDisableSslVerify() && !config.getCertificates().isEmpty()) { + nettyBuilder.tlsTrustManagersProvider(S3TrustManagerProvider.create(config.getCertificates())); + } + SdkAsyncHttpClient nettyClient = nettyBuilder.buildWithDefaults(httpOptions.build()); if (config.useRoundRobinDnsResolver()) { try { @@ -241,6 +246,11 @@ } private static S3AsyncClient createS3CrtAsyncClient(S3ClientConfig config) { + if (!config.getCertificates().isEmpty()) { + LOGGER.warn("Custom CA certificate is not supported with the CRT S3 client. " + + "The certificate will be ignored for parallel downloads. " + + "Consider using the 'async' parallel downloader client type instead."); + } S3CrtAsyncClientBuilder builder = S3AsyncClient.crtBuilder(); builder.credentialsProvider(config.createCredentialsProvider()); builder.region(Region.of(config.getRegion())); @@ -248,6 +258,9 @@ if (config.getEndpoint() != null && !config.getEndpoint().isEmpty()) { builder.endpointOverride(URI.create(config.getEndpoint())); } + if (config.isDisableSslVerify()) { + builder.httpConfiguration(S3CrtHttpConfiguration.builder().trustAllCertificatesEnabled(true).build()); + } return builder.build(); } diff --git a/asterixdb/asterix-cloud/src/main/java/org/apache/asterix/cloud/clients/aws/s3/S3TrustManagerProvider.java b/asterixdb/asterix-cloud/src/main/java/org/apache/asterix/cloud/clients/aws/s3/S3TrustManagerProvider.java new file mode 100644 index 0000000..7100e46 --- /dev/null +++ b/asterixdb/asterix-cloud/src/main/java/org/apache/asterix/cloud/clients/aws/s3/S3TrustManagerProvider.java @@ -0,0 +1,123 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.asterix.cloud.clients.aws.s3; + +import static org.apache.hyracks.util.annotations.AiProvenance.Agent.CLAUDE_OPUS_4_6; +import static org.apache.hyracks.util.annotations.AiProvenance.ContributionKind.GENERATED; +import static org.apache.hyracks.util.annotations.AiProvenance.Tool.GITHUB_COPILOT; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.security.KeyStore; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; + +import org.apache.hyracks.util.annotations.AiProvenance; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import software.amazon.awssdk.http.TlsTrustManagersProvider; + +/** + * Provides a {@link TlsTrustManagersProvider} that trusts the default system certificates + * plus any additional PEM-encoded CA certificates supplied via the blobStorageCertificates setting. + */ +@AiProvenance(agent = CLAUDE_OPUS_4_6, tool = GITHUB_COPILOT, contributionKind = GENERATED) +final class S3TrustManagerProvider { + + private static final Logger LOGGER = LogManager.getLogger(); + + private S3TrustManagerProvider() { + throw new AssertionError("do not instantiate"); + } + + /** + * Creates a {@link TlsTrustManagersProvider} that merges the given PEM certificate(s) + * with the JVM default trust store. + * + * @param pemCertificates PEM-encoded CA certificate(s) + * @return a provider that returns trust managers trusting both the system CAs and the custom certificate(s) + */ + static TlsTrustManagersProvider create(Collection<String> pemCertificates) { + String pemCertificateData = String.join("\n", pemCertificates); + return () -> buildTrustManagers(pemCertificateData); + } + + private static TrustManager[] buildTrustManagers(String pemCertificate) { + try { + // Parse the PEM certificate(s) + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + Collection<? extends java.security.cert.Certificate> certs = + cf.generateCertificates(new ByteArrayInputStream(pemCertificate.getBytes(StandardCharsets.UTF_8))); + if (certs.isEmpty()) { + LOGGER.warn("No certificates found in the provided PEM data; falling back to system defaults"); + return getDefaultTrustManagers(); + } + + // Load the default trust store + TrustManagerFactory defaultTmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + defaultTmf.init((KeyStore) null); + + // Build a new key store with the default certs + custom certs + KeyStore customKeyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + customKeyStore.load(null, null); + + // Import default trusted certs + TrustManager[] defaultTrustManagers = defaultTmf.getTrustManagers(); + for (TrustManager tm : defaultTrustManagers) { + if (tm instanceof javax.net.ssl.X509TrustManager) { + for (X509Certificate cert : ((javax.net.ssl.X509TrustManager) tm).getAcceptedIssuers()) { + String alias = cert.getSubjectX500Principal().getName(); + customKeyStore.setCertificateEntry(alias, cert); + } + } + } + + // Import custom CA certs + List<? extends java.security.cert.Certificate> certList = new ArrayList<>(certs); + for (int i = 0; i < certList.size(); i++) { + customKeyStore.setCertificateEntry("custom-ca-" + i, certList.get(i)); + } + + TrustManagerFactory customTmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + customTmf.init(customKeyStore); + LOGGER.info("Custom CA certificate(s) loaded successfully ({} certificate(s))", certs.size()); + return customTmf.getTrustManagers(); + } catch (Exception e) { + LOGGER.warn("Failed to load custom CA certificate; falling back to system defaults", e); + return getDefaultTrustManagers(); + } + } + + private static TrustManager[] getDefaultTrustManagers() { + try { + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init((KeyStore) null); + return tmf.getTrustManagers(); + } catch (Exception e) { + throw new IllegalStateException("Unable to initialize default TrustManagerFactory", e); + } + } +} diff --git a/asterixdb/asterix-cloud/src/test/java/org/apache/asterix/cloud/s3/LSMS3Test.java b/asterixdb/asterix-cloud/src/test/java/org/apache/asterix/cloud/s3/LSMS3Test.java index 382bb4f..005fade 100644 --- a/asterixdb/asterix-cloud/src/test/java/org/apache/asterix/cloud/s3/LSMS3Test.java +++ b/asterixdb/asterix-cloud/src/test/java/org/apache/asterix/cloud/s3/LSMS3Test.java @@ -19,6 +19,7 @@ package org.apache.asterix.cloud.s3; import java.net.URI; +import java.util.Collections; import org.apache.asterix.cloud.AbstractLSMTest; import org.apache.asterix.cloud.clients.ICloudGuardian; @@ -67,8 +68,9 @@ client.createBucket(CreateBucketRequest.builder().bucket(PLAYGROUND_CONTAINER).build()); LOGGER.info("Client created successfully"); int writeBufferSize = StorageUtil.getIntSizeInBytes(5, StorageUtil.StorageUnit.MEGABYTE); - S3ClientConfig config = new S3ClientConfig(MOCK_SERVER_REGION, MOCK_SERVER_HOSTNAME, "", true, 0, - writeBufferSize, S3ClientConfig.S3ParallelDownloaderClientType.ASYNC, false); + S3ClientConfig config = + new S3ClientConfig(MOCK_SERVER_REGION, MOCK_SERVER_HOSTNAME, "", true, Collections.emptyList(), 0, + writeBufferSize, S3ClientConfig.S3ParallelDownloaderClientType.ASYNC, false); CLOUD_CLIENT = new S3CloudClient(config, ICloudGuardian.NoOpCloudGuardian.INSTANCE); } diff --git a/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/config/CloudProperties.java b/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/config/CloudProperties.java index abe6560..9a7b4ed 100644 --- a/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/config/CloudProperties.java +++ b/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/config/CloudProperties.java @@ -25,9 +25,13 @@ import static org.apache.hyracks.control.common.config.OptionTypes.NONNEGATIVE_INTEGER; import static org.apache.hyracks.control.common.config.OptionTypes.POSITIVE_INTEGER; import static org.apache.hyracks.control.common.config.OptionTypes.STRING; +import static org.apache.hyracks.control.common.config.OptionTypes.STRING_ARRAY; import static org.apache.hyracks.control.common.config.OptionTypes.getRangedIntegerType; import static org.apache.hyracks.util.StorageUtil.StorageUnit.GIGABYTE; +import java.util.Collection; +import java.util.Collections; +import java.util.List; import java.util.concurrent.TimeUnit; import java.util.function.Function; @@ -54,6 +58,7 @@ CLOUD_STORAGE_PREFIX(STRING, ""), CLOUD_STORAGE_REGION(STRING, ""), CLOUD_STORAGE_ENDPOINT(STRING, ""), + CLOUD_STORAGE_CERTIFICATES(STRING_ARRAY, new String[0]), CLOUD_STORAGE_ANONYMOUS_AUTH(BOOLEAN, false), CLOUD_STORAGE_CACHE_POLICY(STRING, "selective"), // 80% of the total disk space @@ -106,6 +111,7 @@ case CLOUD_STORAGE_PREFIX: case CLOUD_STORAGE_REGION: case CLOUD_STORAGE_ENDPOINT: + case CLOUD_STORAGE_CERTIFICATES: case CLOUD_STORAGE_ANONYMOUS_AUTH: case CLOUD_STORAGE_CACHE_POLICY: case CLOUD_STORAGE_ALLOCATION_PERCENTAGE: @@ -147,6 +153,8 @@ return "The cloud storage endpoint"; case CLOUD_STORAGE_ANONYMOUS_AUTH: return "Indicates whether or not anonymous auth should be used for the cloud storage"; + case CLOUD_STORAGE_CERTIFICATES: + return "The certificates to use to validate the cloud storage server"; case CLOUD_STORAGE_CACHE_POLICY: return "The caching policy (either eager, lazy or selective). 'eager' caching will download" + "all partitions upon booting, whereas 'lazy' caching will download a file upon" @@ -254,6 +262,11 @@ return accessor.getBoolean(Option.CLOUD_STORAGE_ANONYMOUS_AUTH); } + public Collection<String> getStorageCertificates() { + String[] certificates = accessor.getStringArray(Option.CLOUD_STORAGE_CERTIFICATES); + return certificates == null || certificates.length == 0 ? Collections.emptyList() : List.of(certificates); + } + public CloudCachePolicy getCloudCachePolicy() { return CloudCachePolicy.fromName(accessor.getString(Option.CLOUD_STORAGE_CACHE_POLICY)); } diff --git a/asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/util/ExternalDataConstants.java b/asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/util/ExternalDataConstants.java index 0eb935e..7b9dca8 100644 --- a/asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/util/ExternalDataConstants.java +++ b/asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/util/ExternalDataConstants.java @@ -354,6 +354,7 @@ public static final String CONTAINER_NAME_FIELD_NAME = "container"; public static final String SUBPATH = "subpath"; public static final String PREFIX_DEFAULT_DELIMITER = "/"; + public static final String CERTIFICATES_FIELD_NAME = "certificates"; public static final String DISABLE_SSL_VERIFY_FIELD_NAME = "disableSslVerify"; public static final Pattern COMPUTED_FIELD_PATTERN = Pattern.compile("\\{[^{}:]+:[^{}:]+}"); diff --git a/asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/util/aws/s3/S3Utils.java b/asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/util/aws/s3/S3Utils.java index 8c81c1b..aa06e98 100644 --- a/asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/util/aws/s3/S3Utils.java +++ b/asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/util/aws/s3/S3Utils.java @@ -20,6 +20,7 @@ import static org.apache.asterix.common.exceptions.ErrorCode.INVALID_PARAM_VALUE_ALLOWED_VALUE; import static org.apache.asterix.common.exceptions.ErrorCode.LONG_LIVED_CREDENTIALS_NEEDED_TO_ASSUME_ROLE; +import static org.apache.asterix.external.util.ExternalDataConstants.CERTIFICATES_FIELD_NAME; import static org.apache.asterix.external.util.ExternalDataUtils.getDisableSslVerify; import static org.apache.asterix.external.util.ExternalDataUtils.getPrefix; import static org.apache.asterix.external.util.ExternalDataUtils.isDeltaTable; @@ -67,7 +68,14 @@ import static org.apache.asterix.external.util.aws.s3.S3Constants.PATH_STYLE_ADDRESSING_FIELD_NAME; import static org.apache.hyracks.api.util.ExceptionUtils.getMessageOrToString; +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -75,6 +83,10 @@ import java.util.function.BiPredicate; import java.util.regex.Matcher; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + import org.apache.asterix.common.api.IApplicationContext; import org.apache.asterix.common.exceptions.CompilationException; import org.apache.asterix.common.exceptions.ErrorCode; @@ -98,6 +110,7 @@ import software.amazon.awssdk.core.exception.SdkException; import software.amazon.awssdk.http.SdkHttpClient; import software.amazon.awssdk.http.SdkHttpConfigurationOption; +import software.amazon.awssdk.http.TlsTrustManagersProvider; import software.amazon.awssdk.http.apache.ApacheHttpClient; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.s3.S3Client; @@ -116,6 +129,19 @@ public class S3Utils { private static final Logger LOGGER = LogManager.getLogger(); + private static final class StaticTrustManagersProvider implements TlsTrustManagersProvider { + private final TrustManager[] trustManagers; + + private StaticTrustManagersProvider(TrustManager[] trustManagers) { + this.trustManagers = trustManagers; + } + + @Override + public TrustManager[] trustManagers() { + return trustManagers; + } + } + private S3Utils() { throw new AssertionError("do not instantiate"); } @@ -138,6 +164,7 @@ boolean crossRegion = getCrossRegion(configuration); boolean disableSslVerify = getDisableSslVerify(configuration); + String certificates = configuration.get(CERTIFICATES_FIELD_NAME); S3ClientBuilder builder = S3Client.builder(); builder.region(region); @@ -149,12 +176,57 @@ builder.forcePathStyle(pathStyleAddressing); if (disableSslVerify) { disableSslVerify(builder); + } else if (certificates != null && !certificates.isBlank()) { + builder.httpClient(createHttpClient(certificates)); } awsClients.setConsumingClient(builder.build()); return awsClients; } + static SdkHttpClient createHttpClient(String pemCertificates) throws CompilationException { + TrustManager[] trustManagers = buildTrustManagers(pemCertificates); + TlsTrustManagersProvider trustManagersProvider = new StaticTrustManagersProvider(trustManagers); + return ApacheHttpClient.builder().tlsTrustManagersProvider(trustManagersProvider).build(); + } + + static TrustManager[] buildTrustManagers(String pemCertificates) throws CompilationException { + try { + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + Collection<? extends Certificate> certificates = certificateFactory.generateCertificates( + new ByteArrayInputStream(pemCertificates.getBytes(StandardCharsets.UTF_8))); + if (certificates.isEmpty()) { + throw new IllegalArgumentException("No certificates found in the supplied PEM data"); + } + + TrustManagerFactory defaultTrustManagerFactory = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + defaultTrustManagerFactory.init((KeyStore) null); + + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(null, null); + + int alias = 0; + for (TrustManager trustManager : defaultTrustManagerFactory.getTrustManagers()) { + if (trustManager instanceof X509TrustManager x509TrustManager) { + for (X509Certificate certificate : x509TrustManager.getAcceptedIssuers()) { + keyStore.setCertificateEntry("default-ca-" + alias++, certificate); + } + } + } + for (Certificate certificate : certificates) { + keyStore.setCertificateEntry("custom-ca-" + alias++, certificate); + } + + TrustManagerFactory trustManagerFactory = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(keyStore); + return trustManagerFactory.getTrustManagers(); + } catch (Exception e) { + throw new CompilationException(ErrorCode.EXTERNAL_SOURCE_ERROR, e, getMessageOrToString(e)); + } + } + private static void disableSslVerify(S3ClientBuilder builder) { AttributeMap.Builder attributeMapBuilder = AttributeMap.builder(); attributeMapBuilder.put(SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES, true); -- To view, visit https://asterix-gerrit.ics.uci.edu/c/asterixdb/+/21028?usp=email To unsubscribe, or for help writing mail filters, visit https://asterix-gerrit.ics.uci.edu/settings?usp=email Gerrit-MessageType: merged Gerrit-Project: asterixdb Gerrit-Branch: phoenix Gerrit-Change-Id: I0af4a59ff219de1a5213d90719a2995f8ebfe7b3 Gerrit-Change-Number: 21028 Gerrit-PatchSet: 7 Gerrit-Owner: Michael Blow <[email protected]> Gerrit-Reviewer: Anon. E. Moose #1000171 Gerrit-Reviewer: Ian Maxon <[email protected]> Gerrit-Reviewer: Jenkins <[email protected]> Gerrit-Reviewer: Michael Blow <[email protected]> Gerrit-Reviewer: Ritik Raj <[email protected]>
