Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "feature",
"category": "Amazon SNS Message Manager",
"contributor": "",
"description": "This change introduces the SNS Message Manager for 2.x, a library used to parse and validate messages received from SNS. This aims to provide the same functionality as [SnsMessageManager](https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/sns/message/SnsMessageManager.html) from 1.x."
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

package software.amazon.awssdk.core.internal.http.loader;

import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.annotations.SdkProtectedApi;
import software.amazon.awssdk.core.exception.SdkClientException;
import software.amazon.awssdk.http.SdkHttpClient;
import software.amazon.awssdk.http.SdkHttpService;
Expand All @@ -24,7 +24,7 @@
/**
* Utility to load the default HTTP client factory and create an instance of {@link SdkHttpClient}.
*/
@SdkInternalApi
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll need to discuss this change, but hopefully not contentious. I didn't want to move it out of the internal package as that would be a breaking change.

@SdkProtectedApi
public final class DefaultSdkHttpClientBuilder implements SdkHttpClient.Builder {

private static final SdkHttpServiceProvider<SdkHttpService> DEFAULT_CHAIN = new CachingSdkHttpServiceProvider<>(
Expand Down
6 changes: 6 additions & 0 deletions services-custom/sns-message-manager/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@
<artifactId>httpclient5</artifactId>
<version>${httpcomponents.client5.version}</version>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>apache5-client</artifactId>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This more or less means we should block this until #6732 is merged

<version>${project.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* 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;

import java.io.InputStream;
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.http.SdkHttpClient;
import software.amazon.awssdk.messagemanager.sns.internal.DefaultSnsMessageManager;
import software.amazon.awssdk.messagemanager.sns.model.SnsMessage;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.utils.SdkAutoCloseable;


/**
* Message manager for validating SNS message signatures. Create an instance using {@link #builder()}.
*
* <p>This manager provides automatic validation of SNS message signatures received via HTTP/HTTPS endpoints,
* ensuring that messages originate from Amazon SNS and have not been modified during transmission.
* It supports both SignatureVersion1 (SHA1) and SignatureVersion2 (SHA256) as per AWS SNS standards.
*
* <p>The manager handles certificate retrieval, caching, and validation automatically, supporting different
* AWS regions and partitions (aws, aws-gov, aws-cn).
*
* <p>Basic usage with default configuration:
* <pre>
* {@code
* SnsMessageManager messageManager = SnsMessageManager.builder().build();
*
* try {
* SnsMessage validatedMessage = messageManager.parseMessage(messageBody);
* String messageContent = validatedMessage.message();
* String topicArn = validatedMessage.topicArn();
* // Process the validated message
* } catch (SdkClientException e) {
* // Handle validation failure
* logger.error("SNS message validation failed: {}", e.getMessage());
* }
* }
* </pre>
*
* <p>Advanced usage with custom HTTP client:
* <pre>
* {@code
* SnsMessageManager messageManager = SnsMessageManager.builder()
* .httpClient(ApacheHttpClient.create())
* .build();
* }
* </pre>
*
* @see SnsMessage
* @see Builder
*/
@SdkPublicApi
public interface SnsMessageManager extends SdkAutoCloseable {

/**
* Creates a builder for configuring and creating an {@link SnsMessageManager}.
*
* @return A new builder.
*/
static Builder builder() {
return DefaultSnsMessageManager.builder();
}

/**
* Parses and validates an SNS message from a stream.
* <p>
* This method reads the JSON message payload, validates the signature, returns a parsed SNS message object with all
* message attributes if validation succeeds.
*/
SnsMessage parseMessage(InputStream messageStream);

/**
* Parses and validates an SNS message from a string.
* <p>
* This method reads the JSON message payload, validates the signature, returns a parsed SNS message object with all
* message attributes if validation succeeds.
*/
SnsMessage parseMessage(String messageContent);

/**
* Close this {@code SnsMessageManager}, releasing any resources it owned.
* <p>
* <b>Note:</b> if you provided your own {@link SdkHttpClient}, you must close it separately.
*/
@Override
void close();

interface Builder {

/**
* Sets the HTTP client to use for certificate retrieval. The caller is responsible for closing this HTTP client after
* the {@code SnsMessageManager} is closed.
*
* @param httpClient The HTTP client to use for fetching signing certificates.
* @return This builder for method chaining.
*/
Builder httpClient(SdkHttpClient httpClient);

/**
* Sets the AWS region for certificate validation. This region must match the SNS region where the messages originate.
*
* @param region The AWS region where the SNS messages originate.
* @return This builder for method chaining.
*/
Builder region(Region region);

/**
* Builds an instance of {@link SnsMessageManager} based on the supplied configurations.
*
* @return An initialized SnsMessageManager ready to validate SNS messages.
*/
SnsMessageManager build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import software.amazon.awssdk.http.SdkHttpRequest;
import software.amazon.awssdk.utils.IoUtils;
import software.amazon.awssdk.utils.Lazy;
import software.amazon.awssdk.utils.SdkAutoCloseable;
import software.amazon.awssdk.utils.Validate;
import software.amazon.awssdk.utils.cache.lru.LruCache;

Expand All @@ -49,7 +50,7 @@
* This class retrieves the certificate used to sign a message, validates it, and caches them for future use.
*/
@SdkInternalApi
public final class CertificateRetriever {
public class CertificateRetriever implements SdkAutoCloseable {
private static final Lazy<Pattern> X509_FORMAT = new Lazy<>(() ->
Pattern.compile(
"^[\\s]*-----BEGIN [A-Z]+-----\\n[A-Za-z\\d+\\/\\n]+[=]{0,2}\\n-----END [A-Z]+-----[\\s]*$"));
Expand All @@ -61,26 +62,31 @@ public final class CertificateRetriever {
private final CertificateUrlValidator certUrlValidator;
private final LruCache<URI, PublicKey> certificateCache;

public CertificateRetriever(SdkHttpClient httpClient, String certCommonName) {
this(httpClient, certCommonName, new CertificateUrlValidator(certCommonName));
public CertificateRetriever(SdkHttpClient httpClient, String certHost, String certCommonName) {
this(httpClient, certCommonName, new CertificateUrlValidator(certHost));
}

CertificateRetriever(SdkHttpClient httpClient, String certCommonName, CertificateUrlValidator certificateUrlValidator) {
this.httpClient = Validate.paramNotNull(httpClient, "httpClient");
this.certCommonName = Validate.paramNotNull(certCommonName, "certCommonName");
this.certificateCache = LruCache.builder(this::getCertificate)
this.certificateCache = LruCache.builder(this::fetchCertificate)
.maxSize(10)
.build();
this.certUrlValidator = Validate.paramNotNull(certificateUrlValidator, "certificateUrlValidator");
}

public byte[] retrieveCertificate(URI certificateUrl) {
public PublicKey retrieveCertificate(URI certificateUrl) {
Validate.paramNotNull(certificateUrl, "certificateUrl");
certUrlValidator.validate(certificateUrl);
return certificateCache.get(certificateUrl).getEncoded();
return certificateCache.get(certificateUrl);
}

private PublicKey getCertificate(URI certificateUrl) {
@Override
public void close() {
httpClient.close();
}

private PublicKey fetchCertificate(URI certificateUrl) {
byte[] cert = fetchUrl(certificateUrl);
validateCertificateData(cert);
return createPublicKey(cert);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,18 @@
import java.net.URI;
import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.core.exception.SdkClientException;
import software.amazon.awssdk.utils.Validate;

/**
* Validates that the signing certificate URL is valid.
*/
@SdkInternalApi
public class CertificateUrlValidator {
private final String expectedCommonName;
private final String certificateHost;

public CertificateUrlValidator(String expectedCommonName) {
this.expectedCommonName = expectedCommonName;
public CertificateUrlValidator(String certificateHost) {
Validate.notBlank(certificateHost, "Expected certificate host cannot be null or empty");
this.certificateHost = certificateHost;
}

public void validate(URI certificateUrl) {
Expand All @@ -39,8 +41,8 @@ public void validate(URI certificateUrl) {
throw SdkClientException.create("Certificate URL must use HTTPS");
}

if (!expectedCommonName.equals(certificateUrl.getHost())) {
throw SdkClientException.create("Certificate URL does not match expected host: " + expectedCommonName);
if (!certificateHost.equals(certificateUrl.getHost())) {
throw SdkClientException.create("Certificate URL does not match expected host: " + certificateHost);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
* 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.io.ByteArrayInputStream;
import java.io.InputStream;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.security.PublicKey;
import java.time.Duration;
import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.annotations.SdkTestInternalApi;
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.messagemanager.sns.SnsMessageManager;
import software.amazon.awssdk.messagemanager.sns.model.SnsMessage;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.utils.AttributeMap;
import software.amazon.awssdk.utils.Validate;

@SdkInternalApi
public final class DefaultSnsMessageManager implements SnsMessageManager {
private static final AttributeMap HTTP_CLIENT_DEFAULTS =
AttributeMap.builder()
.put(SdkHttpConfigurationOption.CONNECTION_TIMEOUT, Duration.ofSeconds(10))
.put(SdkHttpConfigurationOption.READ_TIMEOUT, Duration.ofSeconds(30))
.build();

private final SnsMessageUnmarshaller unmarshaller;
private final CertificateRetriever certRetriever;
private final SignatureValidator signatureValidator;

private DefaultSnsMessageManager(BuilderImpl builder) {
this.unmarshaller = new SnsMessageUnmarshaller();

SnsHostProvider hostProvider = new SnsHostProvider(builder.region);
URI signingCertEndpoint = hostProvider.regionalEndpoint();
String signingCertCommonName = hostProvider.signingCertCommonName();

SdkHttpClient httpClient = resolveHttpClient(builder);
certRetriever = builder.certRetriever != null
? builder.certRetriever
: new CertificateRetriever(httpClient, signingCertEndpoint.getHost(), signingCertCommonName);

signatureValidator = new SignatureValidator();
}

@Override
public SnsMessage parseMessage(InputStream message) {
Validate.notNull(message, "message cannot be null");

SnsMessage snsMessage = unmarshaller.unmarshall(message);
PublicKey certificate = certRetriever.retrieveCertificate(snsMessage.signingCertUrl());

signatureValidator.validateSignature(snsMessage, certificate);

return snsMessage;
}

@Override
public SnsMessage parseMessage(String message) {
Validate.notNull(message, "message cannot be null");
return parseMessage(new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8)));
}

@Override
public void close() {
certRetriever.close();
}

public static Builder builder() {
return new BuilderImpl();
}

private static SdkHttpClient resolveHttpClient(BuilderImpl builder) {
if (builder.httpClient != null) {
return new UnmanagedSdkHttpClient(builder.httpClient);
}

return new DefaultSdkHttpClientBuilder().buildWithDefaults(HTTP_CLIENT_DEFAULTS);
}

static class BuilderImpl implements SnsMessageManager.Builder {
private Region region;
private SdkHttpClient httpClient;

// Testing only
private CertificateRetriever certRetriever;

@Override
public Builder httpClient(SdkHttpClient httpClient) {
this.httpClient = httpClient;
return this;
}

@Override
public Builder region(Region region) {
this.region = region;
return this;
}

@SdkTestInternalApi
Builder certificateRetriever(CertificateRetriever certificateRetriever) {
this.certRetriever = certificateRetriever;
return this;
}

@Override
public SnsMessageManager build() {
return new DefaultSnsMessageManager(this);
}
}
}
Loading
Loading