From decded3da3d94f4f6ebc60710d4d38e6ad95e011 Mon Sep 17 00:00:00 2001 From: Dongie Agnir Date: Fri, 13 Mar 2026 16:02:23 -0700 Subject: [PATCH 1/2] Implement host and CN resolution SnsHostProvider implements the logic to determine the SNS endpoint for a given region, as well as the expected common name of a signing certificate used by SNS in that region. Both pieces of information used to ensure that the certificate we use to verify the message signature is legitimate. --- .../amazon/awssdk/spotbugs-suppressions.xml | 2 + services-custom/sns-message-manager/pom.xml | 19 ++- .../sns/internal/SnsHostProvider.java | 125 ++++++++++++++++++ .../sns/internal/SnsHostProviderTest.java | 117 ++++++++++++++++ 4 files changed, 261 insertions(+), 2 deletions(-) create mode 100644 services-custom/sns-message-manager/src/main/java/software/amazon/awssdk/messagemanager/sns/internal/SnsHostProvider.java create mode 100644 services-custom/sns-message-manager/src/test/java/software/amazon/awssdk/messagemanager/sns/internal/SnsHostProviderTest.java diff --git a/build-tools/src/main/resources/software/amazon/awssdk/spotbugs-suppressions.xml b/build-tools/src/main/resources/software/amazon/awssdk/spotbugs-suppressions.xml index 6871e760a793..8130fa7245af 100644 --- a/build-tools/src/main/resources/software/amazon/awssdk/spotbugs-suppressions.xml +++ b/build-tools/src/main/resources/software/amazon/awssdk/spotbugs-suppressions.xml @@ -346,6 +346,8 @@ + + diff --git a/services-custom/sns-message-manager/pom.xml b/services-custom/sns-message-manager/pom.xml index da52f609a1e1..fb5619d5e5c9 100644 --- a/services-custom/sns-message-manager/pom.xml +++ b/services-custom/sns-message-manager/pom.xml @@ -21,7 +21,7 @@ software.amazon.awssdk aws-sdk-java-pom - 2.42.3-SNAPSHOT + 2.42.15-SNAPSHOT ../../pom.xml sns-message-manager @@ -68,11 +68,26 @@ sdk-core ${project.version} + + software.amazon.awssdk + regions + ${project.version} + + + software.amazon.awssdk + sns + ${project.version} + software.amazon.awssdk http-client-spi ${project.version} + + software.amazon.awssdk + endpoints-spi + ${project.version} + org.apache.httpcomponents.client5 httpclient5 @@ -80,7 +95,7 @@ org.junit.jupiter - junit-jupiter-engine + junit-jupiter test diff --git a/services-custom/sns-message-manager/src/main/java/software/amazon/awssdk/messagemanager/sns/internal/SnsHostProvider.java b/services-custom/sns-message-manager/src/main/java/software/amazon/awssdk/messagemanager/sns/internal/SnsHostProvider.java new file mode 100644 index 000000000000..28120ecf198c --- /dev/null +++ b/services-custom/sns-message-manager/src/main/java/software/amazon/awssdk/messagemanager/sns/internal/SnsHostProvider.java @@ -0,0 +1,125 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.messagemanager.sns.internal; + +import java.net.URI; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.annotations.SdkTestInternalApi; +import software.amazon.awssdk.annotations.ThreadSafe; +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.endpoints.Endpoint; +import software.amazon.awssdk.regions.PartitionMetadata; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.sns.endpoints.SnsEndpointParams; +import software.amazon.awssdk.services.sns.endpoints.SnsEndpointProvider; +import software.amazon.awssdk.utils.CompletableFutureUtils; +import software.amazon.awssdk.utils.Logger; + +/** + * Utility class for determining both the regional endpoint that SNS certificates are expected to be hosted from, as well as the + * expected common name (CN) that the certificate from that endpoint must have. + */ +@SdkInternalApi +@ThreadSafe +public class SnsHostProvider { + private static final Logger LOG = Logger.loggerFor(SnsHostProvider.class); + + private final Region region; + private final SnsEndpointProvider endpointProvider; + + public SnsHostProvider(Region region) { + this(region, SnsEndpointProvider.defaultProvider()); + } + + @SdkTestInternalApi + SnsHostProvider(Region region, SnsEndpointProvider endpointProvider) { + this.region = region; + this.endpointProvider = endpointProvider; + } + + public URI regionalEndpoint() { + SnsEndpointParams params = SnsEndpointParams.builder().region(region).build(); + try { + Endpoint endpoint = CompletableFutureUtils.joinLikeSync(endpointProvider.resolveEndpoint(params)); + URI url = endpoint.url(); + LOG.debug(() -> String.format("Resolved endpoint %s for region %s", url, region)); + return url; + } catch (SdkClientException e) { + throw SdkClientException.create("Unable to resolve SNS endpoint for region " + region, e); + } + } + + public String signingCertCommonName() { + String commonName = signingCertCommonNameInternal(); + LOG.debug(() -> String.format("Resolved common name %s for region %s", commonName, region)); + return commonName; + } + + private String signingCertCommonNameInternal() { + // If we don't know about this region, try to guess common name + if (!Region.regions().contains(region)) { + // Find the partition where it belongs by checking the region against the published pattern for known partitions. + // e.g. 'us-gov-west-3' would match the 'aws-us-gov' partition. + // This will return the 'aws' partition if it fails to match any partition. + PartitionMetadata partitionMetadata = PartitionMetadata.of(region); + return "sns." + partitionMetadata.dnsSuffix(); + } + + String regionId = region.id(); + + switch (regionId) { + case "cn-north-1": + return "sns-cn-north-1.amazonaws.com.cn"; + case "cn-northwest-1": + return "sns-cn-northwest-1.amazonaws.com.cn"; + case "us-gov-west-1": + case "us-gov-east-1": + return "sns-us-gov-west-1.amazonaws.com"; + case "us-iso-east-1": + return "sns-us-iso-east-1.c2s.ic.gov"; + case "us-isob-east-1": + return "sns-us-isob-east-1.sc2s.sgov.gov"; + case "us-isof-east-1": + return "sns-signing.us-isof-east-1.csp.hci.ic.gov"; + case "us-isof-south-1": + return "sns-signing.us-isof-south-1.csp.hci.ic.gov"; + case "eu-isoe-west-1": + return "sns-signing.eu-isoe-west-1.cloud.adc-e.uk"; + case "eusc-de-east-1": + return "sns-signing.eusc-de-east-1.amazonaws.eu"; + case "ap-east-1": + case "ap-east-2": + case "ap-south-2": + case "ap-southeast-5": + case "ap-southeast-6": + case "ap-southeast-7": + case "me-south-1": + case "me-central-1": + case "eu-south-1": + case "eu-south-2": + case "eu-central-2": + case "af-south-1": + case "ap-southeast-3": + case "ap-southeast-4": + case "il-central-1": + case "ca-west-1": + case "mx-central-1": + return "sns-signing." + regionId + ".amazonaws.com"; + default: + return "sns.amazonaws.com"; + } + } +} diff --git a/services-custom/sns-message-manager/src/test/java/software/amazon/awssdk/messagemanager/sns/internal/SnsHostProviderTest.java b/services-custom/sns-message-manager/src/test/java/software/amazon/awssdk/messagemanager/sns/internal/SnsHostProviderTest.java new file mode 100644 index 000000000000..e1dcc29b34a2 --- /dev/null +++ b/services-custom/sns-message-manager/src/test/java/software/amazon/awssdk/messagemanager/sns/internal/SnsHostProviderTest.java @@ -0,0 +1,117 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.messagemanager.sns.internal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.ArgumentCaptor; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.sns.endpoints.SnsEndpointParams; +import software.amazon.awssdk.services.sns.endpoints.SnsEndpointProvider; + +public class SnsHostProviderTest { + + @ParameterizedTest + @MethodSource("commonNameTestCases") + void signingCertCommonName_returnsCorrectNameForRegion(CommonNameTestCase tc) { + SnsHostProvider hostProvider = new SnsHostProvider(Region.of(tc.region)); + assertThat(hostProvider.signingCertCommonName()).isEqualTo(tc.expectedCommonName); + } + + @Test + void regionalEndpoint_delegatesToEndpointProvider() { + SnsEndpointProvider mockProvider = mock(SnsEndpointProvider.class); + SnsEndpointProvider realProvider = SnsEndpointProvider.defaultProvider(); + + when(mockProvider.resolveEndpoint(any(SnsEndpointParams.class))).thenAnswer( + i -> realProvider.resolveEndpoint(i.getArgument(0, SnsEndpointParams.class))); + + ArgumentCaptor paramsCaptor = ArgumentCaptor.forClass(SnsEndpointParams.class); + + Region region = Region.US_WEST_2; + SnsHostProvider hostProvider = new SnsHostProvider(region, mockProvider); + hostProvider.regionalEndpoint(); + + verify(mockProvider).resolveEndpoint(paramsCaptor.capture()); + assertThat(paramsCaptor.getValue().region()).isEqualTo(region); + } + + private static Stream commonNameTestCases() { + return Stream.of( + // gov regions + new CommonNameTestCase("us-gov-west-1", "sns-us-gov-west-1.amazonaws.com"), + new CommonNameTestCase("us-gov-east-1", "sns-us-gov-west-1.amazonaws.com"), + + // cn regions + new CommonNameTestCase("cn-north-1", "sns-cn-north-1.amazonaws.com.cn"), + new CommonNameTestCase("cn-northwest-1", "sns-cn-northwest-1.amazonaws.com.cn"), + + // opt-in regions + new CommonNameTestCase("me-south-1", "sns-signing.me-south-1.amazonaws.com"), + new CommonNameTestCase("ap-east-1", "sns-signing.ap-east-1.amazonaws.com"), + new CommonNameTestCase("me-south-1", "sns-signing.me-south-1.amazonaws.com"), + new CommonNameTestCase("ap-east-2", "sns-signing.ap-east-2.amazonaws.com"), + new CommonNameTestCase("ap-southeast-5", "sns-signing.ap-southeast-5.amazonaws.com"), + new CommonNameTestCase("ap-southeast-6", "sns-signing.ap-southeast-6.amazonaws.com"), + new CommonNameTestCase("ap-southeast-7", "sns-signing.ap-southeast-7.amazonaws.com"), + new CommonNameTestCase("mx-central-1", "sns-signing.mx-central-1.amazonaws.com"), + + // iso regions + new CommonNameTestCase("us-iso-east-1", "sns-us-iso-east-1.c2s.ic.gov"), + new CommonNameTestCase("us-isob-east-1", "sns-us-isob-east-1.sc2s.sgov.gov"), + new CommonNameTestCase("us-isof-east-1", "sns-signing.us-isof-east-1.csp.hci.ic.gov"), + new CommonNameTestCase("us-isof-south-1", "sns-signing.us-isof-south-1.csp.hci.ic.gov"), + new CommonNameTestCase("eu-isoe-west-1", "sns-signing.eu-isoe-west-1.cloud.adc-e.uk"), + + //eusc + new CommonNameTestCase("eusc-de-east-1", "sns-signing.eusc-de-east-1.amazonaws.eu"), + + // other regions + new CommonNameTestCase("us-east-1", "sns.amazonaws.com"), + new CommonNameTestCase("us-west-1", "sns.amazonaws.com"), + + // unknown regions + new CommonNameTestCase("us-east-9", "sns.amazonaws.com"), + new CommonNameTestCase("foo-bar-1", "sns.amazonaws.com"), + new CommonNameTestCase("cn-northwest-9", "sns.amazonaws.com.cn") + + + ); + } + + private static class CommonNameTestCase { + private String region; + private String expectedCommonName; + + CommonNameTestCase(String region, String expectedCommonName) { + this.region = region; + this.expectedCommonName = expectedCommonName; + } + + @Override + public String toString() { + return region + " - " + expectedCommonName; + } + } +} From 4ead22c60c804b6fba2efe810d3a7b0bbe44f706 Mon Sep 17 00:00:00 2001 From: Dongie Agnir Date: Tue, 17 Mar 2026 16:12:19 -0700 Subject: [PATCH 2/2] Remove use of internal API Note: This is in codee that will be deleted. --- .../sns/internal/messagemanager/DefaultSnsMessageManager.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/services/sns/src/main/java/software/amazon/awssdk/services/sns/internal/messagemanager/DefaultSnsMessageManager.java b/services/sns/src/main/java/software/amazon/awssdk/services/sns/internal/messagemanager/DefaultSnsMessageManager.java index ae2b61aa7679..871e9126ab53 100644 --- a/services/sns/src/main/java/software/amazon/awssdk/services/sns/internal/messagemanager/DefaultSnsMessageManager.java +++ b/services/sns/src/main/java/software/amazon/awssdk/services/sns/internal/messagemanager/DefaultSnsMessageManager.java @@ -20,7 +20,6 @@ import java.io.InputStream; import java.nio.charset.StandardCharsets; import software.amazon.awssdk.annotations.SdkInternalApi; -import software.amazon.awssdk.core.internal.http.loader.DefaultSdkHttpClientBuilder; import software.amazon.awssdk.http.SdkHttpClient; import software.amazon.awssdk.http.SdkHttpConfigurationOption; import software.amazon.awssdk.services.sns.messagemanager.MessageManagerConfiguration; @@ -88,7 +87,7 @@ private DefaultSnsMessageManager(DefaultBuilder builder) { this.httpClient = configuration.httpClient(); this.shouldCloseHttpClient = false; } else { - this.httpClient = new DefaultSdkHttpClientBuilder().buildWithDefaults(createHttpDefaults()); + this.httpClient = null; this.shouldCloseHttpClient = true; }