diff --git a/.kiro/hooks/aws-sdk-code-review.kiro.hook b/.kiro/hooks/aws-sdk-code-review.kiro.hook
deleted file mode 100644
index 729dc463e5ed..000000000000
--- a/.kiro/hooks/aws-sdk-code-review.kiro.hook
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "enabled": true,
- "name": "AWS SDK Java v2 Code Review",
- "description": "Performs manual code review leveraging all steering documentation to verify code against AWS SDK Java v2 guidelines, best practices, and architectural standards",
- "version": "1",
- "when": {
- "type": "userTriggered",
- "patterns": [
- "**/*.java",
- "**/*.md",
- "**/*.json"
- ]
- },
- "then": {
- "type": "askAgent",
- "prompt": "# AWS SDK Java v2 Code Review\n\nYou are reviewing code for the AWS SDK for Java v2 project. Use the steering documentation in `.kiro/steering/` to apply context-specific guidelines based on file patterns.\n\n## Review Scope\nAnalyze the **changed files** in the current workspace and apply appropriate guidelines based on file types using the steering documentation.\n\n## Guidelines Application\nLoad and apply guidelines from the steering documentation based on file patterns:\n\n- `.kiro/steering/aws-sdk-java-v2-general.md` - For all `**/*.java` files\n- `.kiro/steering/logging-guidelines.md` - For all `**/*.java` files \n- `.kiro/steering/client-configuration-guidelines.md` - For `**/*{Config,Configuration,Builder}*.java`\n- `.kiro/steering/async-programming-guidelines.md` - For `**/*{Async,CompletableFuture}*.java`\n- `.kiro/steering/reactive-streams-guidelines.md` - For `**/*{Publisher,Subscriber}*.java`\n- `.kiro/steering/testing-guidelines.md` - For `**/{test,it}/**/*.java`\n- `.kiro/steering/javadoc-guidelines.md` - For `**/src/main/**/*.java`\n- `.kiro/steering/code-generation-guidelines.md` - For `{codegen/**/*.java,**/poet/**/*.java}`\n\n## Review Process\n1. **Load relevant steering docs** for each changed file based on its path pattern\n2. **Apply all applicable guidelines** from the loaded documentation\n3. **Categorize findings** by severity:\n - ā **CRITICAL**: Must fix before merge (violations of MUST requirements)\n - ā ļø **GUIDELINE**: Should fix for consistency (violations of SHOULD requirements)\n - š” **SUGGESTION**: Consider for improvement\n - ā
**COMPLIANT**: Follows guidelines correctly\n\n## Output Format\n\nFor each file reviewed, provide:\nš [File Path]\nType: [Configuration/Async/Test/General/etc.] Compliance: [Percentage]%\n\nIssues Found:\n- ā CRITICAL: [Issue description]\n - Guideline: [Reference to specific steering doc section]\n - Fix: [Specific suggestion]\n\n- ā ļø GUIDELINE: [Issue description]\n - Reference: [Steering doc reference]\n - Suggestion: [How to improve]\n-š” SUGGESTION: [Improvement opportunity]\n\n## Summary Report\n- **Changed Files Reviewed**: X\n- **Guidelines Applied**: [List of steering docs used]\n- **Overall Compliance**: X%\n- **Critical Issues**: X (must fix before merge)\n- **Guideline Violations**: X (should fix for consistency)\n- **Ready for Review**: [Yes/No based on critical issues]\n\nProvide actionable feedback based on the comprehensive guidelines in the steering documentation, helping developers understand what to fix and why it matters for the AWS SDK project.\n"
- }
-}
\ No newline at end of file
diff --git a/.kiro/hooks/javadoc-manual-trigger.kiro.hook b/.kiro/hooks/javadoc-manual-trigger.kiro.hook
deleted file mode 100644
index 52b63a62e6da..000000000000
--- a/.kiro/hooks/javadoc-manual-trigger.kiro.hook
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "enabled": false,
- "name": "Manual Javadoc Generator",
- "description": "A manual trigger to add comprehensive Javadoc documentation for Java files following the javadoc-guidelines.md standards",
- "version": "1",
- "when": {
- "type": "fileEdited",
- "patterns": [
- "**/*.java"
- ]
- },
- "then": {
- "type": "askAgent",
- "prompt": "Please add comprehensive Javadoc documentation to the Java files that have been modified. Follow the guidelines specified in javadoc-guidelines.md. Focus on:\n\n1. Class-level documentation explaining the purpose and usage\n2. Method documentation with @param, @return, and @throws tags where appropriate\n3. Field documentation for public/protected fields\n4. Code examples where helpful\n5. Proper formatting and professional tone\n\nEnsure the documentation is clear, concise, and follows Java documentation best practices as outlined in the guidelines."
- }
-}
\ No newline at end of file
diff --git a/services-custom/sns-message-manager/pom.xml b/services-custom/sns-message-manager/pom.xml
index f3aa952f2541..d99a51798201 100644
--- a/services-custom/sns-message-manager/pom.xml
+++ b/services-custom/sns-message-manager/pom.xml
@@ -21,7 +21,7 @@
This class handles secure retrieval and caching of SNS signing certificates from AWS. - * It implements comprehensive security validations to ensure certificate authenticity and - * prevent various attack vectors including certificate spoofing and man-in-the-middle attacks. - * - *
Security Features: - *
Trusted Domains: - * The retriever only accepts certificates from pre-validated SNS domains including: - *
Thread Safety: - * This class is thread-safe and can be used concurrently from multiple threads. - * Certificate caching is implemented using thread-safe collections. - * - *
Usage:
- * This class is intended for internal use by the SNS message manager and should not be
- * used directly by client code. Certificates are automatically retrieved and cached
- * during message signature validation.
- *
- * @see SignatureValidator
- * @see DefaultSnsMessageManager
- */
-@SdkInternalApi
-public final class CertificateRetriever {
-
- // Trusted SNS domain patterns for different AWS partitions
- private static final Pattern[] TRUSTED_SNS_DOMAIN_PATTERNS = {
- // AWS Standard partition: sns.
- * This method performs comprehensive security checks:
- *
- * This method validates against known AWS SNS domain patterns for all partitions:
- *
- * The patterns ensure that:
- *
- * This method performs additional security checks on the certificate content
- * to ensure it meets security requirements and is not malformed or malicious.
- *
- * @param certificateBytes The certificate bytes to validate.
- * @throws SnsCertificateException If certificate content validation fails.
- */
- private void validateCertificateContent(byte[] certificateBytes) {
- // Check for minimum certificate size (too small indicates potential issues)
- if (certificateBytes.length < 100) {
- throw SnsCertificateException.builder()
- .message("Certificate is too small (" + certificateBytes.length + " bytes). " +
- "Valid X.509 certificates should be at least 100 bytes")
- .build();
- }
-
- // Validate certificate starts with expected X.509 PEM or DER format markers
- if (!isValidCertificateFormat(certificateBytes)) {
- throw SnsCertificateException.builder()
- .message("Certificate does not appear to be in valid X.509 PEM or DER format")
- .build();
- }
-
- // Check for suspicious content patterns that might indicate tampering
- validateCertificateIntegrity(certificateBytes);
- }
-
- /**
- * Validates that the certificate is in a recognized X.509 format.
- *
- * @param certificateBytes The certificate bytes to check.
- * @return true if the format appears valid, false otherwise.
- */
- private boolean isValidCertificateFormat(byte[] certificateBytes) {
- if (certificateBytes.length < 10) {
- return false;
- }
-
- // Check for PEM format (starts with "-----BEGIN CERTIFICATE-----")
- String beginPem = "-----BEGIN CERTIFICATE-----";
- if (certificateBytes.length >= beginPem.length()) {
- String start = new String(certificateBytes, 0, beginPem.length(), StandardCharsets.US_ASCII);
- if (beginPem.equals(start)) {
- return true;
- }
- }
-
- // Check for DER format (starts with ASN.1 SEQUENCE tag 0x30)
- if (certificateBytes[0] == 0x30) {
- // Basic DER validation - second byte should indicate length encoding
- if (certificateBytes.length > 1) {
- byte lengthByte = certificateBytes[1];
- // Length byte should be reasonable for certificate size
- return (lengthByte & 0x80) == 0 || (lengthByte & 0x7F) <= 4;
- }
- }
-
- return false;
- }
-
- /**
- * Validates certificate integrity by checking for suspicious patterns.
- *
- * @param certificateBytes The certificate bytes to validate.
- * @throws SnsCertificateException If suspicious patterns are detected.
- */
- private void validateCertificateIntegrity(byte[] certificateBytes) {
- // Check for excessive null bytes which might indicate padding attacks
- int nullByteCount = 0;
- int consecutiveNullBytes = 0;
- int maxConsecutiveNullBytes = 0;
-
- for (byte b : certificateBytes) {
- if (b == 0) {
- nullByteCount++;
- consecutiveNullBytes++;
- maxConsecutiveNullBytes = Math.max(maxConsecutiveNullBytes, consecutiveNullBytes);
- } else {
- consecutiveNullBytes = 0;
- }
- }
-
- // If more than 10% of the certificate is null bytes, it's suspicious
- if (nullByteCount > certificateBytes.length * 0.1) {
- throw SnsCertificateException.builder()
- .message("Certificate contains excessive null bytes (" + nullByteCount + " out of " +
- certificateBytes.length + "), which may indicate tampering")
- .build();
- }
-
- // If there are more than 50 consecutive null bytes, it's suspicious
- if (maxConsecutiveNullBytes > 50) {
- throw SnsCertificateException.builder()
- .message("Certificate contains " + maxConsecutiveNullBytes +
- " consecutive null bytes, which may indicate tampering")
- .build();
- }
- }
-
- /**
- * Clears the certificate cache.
- *
- * This method is primarily intended for testing purposes.
- */
- void clearCache() {
- certificateCache.clear();
- }
-
- /**
- * Returns the current cache size.
- *
- * This method is primarily intended for testing purposes.
- *
- * @return The number of cached certificates.
- */
- int getCacheSize() {
- return certificateCache.size();
- }
-
- /**
- * Cached certificate with expiration time.
- */
- private static final class CachedCertificate {
- private final byte[] certificateBytes;
- private final Instant expirationTime;
-
- CachedCertificate(byte[] certificateBytes, Duration cacheTimeout) {
- this.certificateBytes = certificateBytes.clone();
- this.expirationTime = Instant.now().plus(cacheTimeout);
- }
-
- byte[] getCertificateBytes() {
- return certificateBytes.clone();
- }
-
- boolean isExpired() {
- return Instant.now().isAfter(expirationTime);
- }
- }
-}
\ No newline at end of file
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
deleted file mode 100644
index 871e9126ab53..000000000000
--- a/services/sns/src/main/java/software/amazon/awssdk/services/sns/internal/messagemanager/DefaultSnsMessageManager.java
+++ /dev/null
@@ -1,288 +0,0 @@
-/*
- * 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.services.sns.internal.messagemanager;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.charset.StandardCharsets;
-import software.amazon.awssdk.annotations.SdkInternalApi;
-import software.amazon.awssdk.http.SdkHttpClient;
-import software.amazon.awssdk.http.SdkHttpConfigurationOption;
-import software.amazon.awssdk.services.sns.messagemanager.MessageManagerConfiguration;
-import software.amazon.awssdk.services.sns.messagemanager.SnsCertificateException;
-import software.amazon.awssdk.services.sns.messagemanager.SnsMessage;
-import software.amazon.awssdk.services.sns.messagemanager.SnsMessageManager;
-import software.amazon.awssdk.services.sns.messagemanager.SnsMessageParsingException;
-import software.amazon.awssdk.services.sns.messagemanager.SnsSignatureValidationException;
-import software.amazon.awssdk.utils.AttributeMap;
-
-
-/**
- * Default implementation of {@link SnsMessageManager} that provides comprehensive SNS message validation.
- *
- * This class coordinates between the message parser, signature validator, and certificate retriever
- * to provide complete SNS message validation functionality. It handles the entire validation pipeline
- * including JSON parsing, certificate retrieval and caching, and cryptographic signature verification.
- *
- * The implementation supports:
- * This class manages the lifecycle of HTTP resources and implements {@link SdkAutoCloseable}
- * to ensure proper cleanup. When using a custom HTTP client via configuration, the client's
- * lifecycle is managed externally. When using the default HTTP client, this class manages
- * the client's lifecycle and closes it when {@link #close()} is called.
- *
- * Thread Safety: This class is thread-safe and can be used concurrently
- * from multiple threads. Certificate caching is implemented using thread-safe collections.
- *
- * Resource Management: Instances should be closed when no longer needed
- * to free HTTP client resources. Use try-with-resources or explicit close() calls.
- *
- * @see SnsMessageManager
- * @see MessageManagerConfiguration
- * @see SnsMessage
- */
-@SdkInternalApi
-public final class DefaultSnsMessageManager implements SnsMessageManager {
-
- /** The configuration settings for this message manager instance. */
- private final MessageManagerConfiguration configuration;
-
- /** Certificate retriever for fetching and caching SNS signing certificates. */
- private final CertificateRetriever certificateRetriever;
-
- /** HTTP client used for certificate retrieval operations. */
- private final SdkHttpClient httpClient;
-
- /** Flag indicating whether this instance should close the HTTP client on cleanup. */
- private final boolean shouldCloseHttpClient;
-
- private DefaultSnsMessageManager(DefaultBuilder builder) {
- this.configuration = builder.configuration != null
- ? builder.configuration
- : MessageManagerConfiguration.builder().build();
-
- // Initialize HTTP client - use provided one or create default
- if (configuration.httpClient() != null) {
- this.httpClient = configuration.httpClient();
- this.shouldCloseHttpClient = false;
- } else {
- this.httpClient = null;
- this.shouldCloseHttpClient = true;
- }
-
- // Initialize certificate retriever
- this.certificateRetriever = new CertificateRetriever(httpClient, configuration.certificateCacheTimeout());
- }
-
- /**
- * Creates a new builder for {@link DefaultSnsMessageManager}.
- *
- * @return A new builder instance.
- */
- public static Builder builder() {
- return new DefaultBuilder();
- }
-
- @Override
- public SnsMessage parseMessage(InputStream messageStream) {
- // Comprehensive input validation
- validateInputStreamParameter(messageStream);
-
- try {
- String messageContent = readInputStreamToString(messageStream);
- return parseMessage(messageContent);
- } catch (IOException e) {
- throw SnsMessageParsingException.builder()
- .message("Failed to read message from InputStream. This may indicate a network issue, " +
- "stream corruption, or insufficient memory. Error: " + e.getMessage())
- .cause(e)
- .build();
- }
- }
-
- @Override
- public SnsMessage parseMessage(String messageContent) {
- // Comprehensive input validation with detailed error messages
- validateStringMessageParameter(messageContent);
-
- try {
- // Step 1: Parse the JSON message
- SnsMessage parsedMessage = SnsMessageParser.parseMessage(messageContent);
-
- // Step 2: Retrieve the certificate
- byte[] certificateBytes = certificateRetriever.retrieveCertificate(parsedMessage.signingCertUrl());
-
- // Step 3: Validate the signature
- SignatureValidator.validateSignature(parsedMessage, certificateBytes);
-
- // Return the validated message
- return parsedMessage;
-
- } catch (SnsMessageParsingException | SnsSignatureValidationException | SnsCertificateException e) {
- // Let SNS-specific exceptions propagate as-is with their original detailed messages
- throw e;
- } catch (Exception e) {
- // Only wrap truly unexpected exceptions
- throw SnsMessageParsingException.builder()
- .message("Unexpected error during message validation: " + e.getMessage() +
- ". Please check that the message is a valid SNS message and try again.")
- .cause(e)
- .build();
- }
- }
-
- @Override
- public void close() {
- // Close HTTP client only if we created it
- if (shouldCloseHttpClient && httpClient != null) {
- try {
- httpClient.close();
- } catch (Exception e) {
- // Log and ignore - we're closing anyway
- // In a real implementation, this would use a logger
- }
- }
- }
-
- /**
- * Validates the InputStream parameter with comprehensive error reporting.
- *
- * @param messageStream The InputStream to validate.
- * @throws SnsMessageParsingException If validation fails.
- */
- private void validateInputStreamParameter(InputStream messageStream) {
- if (messageStream == null) {
- throw SnsMessageParsingException.builder()
- .message("Message InputStream cannot be null. Please provide a valid InputStream containing SNS message data.")
- .build();
- }
-
- // Additional validation could be added here for stream state if needed
- }
-
- /**
- * Validates the String message parameter with comprehensive error reporting.
- *
- * @param messageContent The message content to validate.
- * @throws SnsMessageParsingException If validation fails.
- */
- private void validateStringMessageParameter(String messageContent) {
- if (messageContent == null) {
- throw SnsMessageParsingException.builder()
- .message("Message content cannot be null. Please provide a valid SNS message JSON string.")
- .build();
- }
-
- if (messageContent.trim().isEmpty()) {
- throw SnsMessageParsingException.builder()
- .message("Message content cannot be empty or contain only whitespace. " +
- "Please provide a valid SNS message JSON string.")
- .build();
- }
-
- // Check for reasonable message size limits
- if (messageContent.length() > 256 * 1024) { // 256KB limit
- throw SnsMessageParsingException.builder()
- .message("Message content is too large (" + messageContent.length() + " characters). " +
- "SNS messages should typically be under 256KB. Please verify the message content.")
- .build();
- }
-
- // Basic format validation - should look like JSON
- String trimmed = messageContent.trim();
- if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) {
- throw SnsMessageParsingException.builder()
- .message("Message content does not appear to be valid JSON. " +
- "SNS messages must be in JSON format starting with '{' and ending with '}'. " +
- "Received content starts with: " +
- (trimmed.length() > 50 ? trimmed.substring(0, 50) + "..." : trimmed))
- .build();
- }
- }
-
- /**
- * Reads an InputStream to a String using UTF-8 encoding with enhanced error handling.
- *
- * @param inputStream The InputStream to read.
- * @return The string content.
- * @throws IOException If reading fails.
- */
- private String readInputStreamToString(InputStream inputStream) throws IOException {
- try (ByteArrayOutputStream result = new ByteArrayOutputStream()) {
- // Read with size limit to prevent memory exhaustion
- byte[] buffer = new byte[8192];
- int totalBytesRead = 0;
- int maxSize = 256 * 1024; // 256KB limit
- int bytesRead;
-
- while ((bytesRead = inputStream.read(buffer)) != -1) {
- totalBytesRead += bytesRead;
-
- if (totalBytesRead > maxSize) {
- throw new IOException("InputStream content exceeds maximum allowed size of " + maxSize + " bytes. " +
- "SNS messages should typically be much smaller.");
- }
-
- result.write(buffer, 0, bytesRead);
- }
-
- if (totalBytesRead == 0) {
- throw new IOException("InputStream is empty. Please provide a valid InputStream containing SNS message data.");
- }
-
- return result.toString(StandardCharsets.UTF_8.name());
- }
- }
-
- /**
- * Creates HTTP defaults for the SNS message manager.
- */
- private static AttributeMap createHttpDefaults() {
- return AttributeMap.builder()
- .put(SdkHttpConfigurationOption.CONNECTION_TIMEOUT, java.time.Duration.ofSeconds(10))
- .put(SdkHttpConfigurationOption.READ_TIMEOUT, java.time.Duration.ofSeconds(30))
- .build();
- }
-
- /**
- * Builder implementation for {@link DefaultSnsMessageManager}.
- */
- public static final class DefaultBuilder implements Builder {
- private MessageManagerConfiguration configuration;
-
- private DefaultBuilder() {
- }
-
- @Override
- public Builder configuration(MessageManagerConfiguration configuration) {
- this.configuration = configuration;
- return this;
- }
-
- @Override
- public SnsMessageManager build() {
- return new DefaultSnsMessageManager(this);
- }
- }
-}
\ No newline at end of file
diff --git a/services/sns/src/main/java/software/amazon/awssdk/services/sns/internal/messagemanager/SignatureValidator.java b/services/sns/src/main/java/software/amazon/awssdk/services/sns/internal/messagemanager/SignatureValidator.java
deleted file mode 100644
index 742b73bf50d0..000000000000
--- a/services/sns/src/main/java/software/amazon/awssdk/services/sns/internal/messagemanager/SignatureValidator.java
+++ /dev/null
@@ -1,361 +0,0 @@
-/*
- * 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.services.sns.internal.messagemanager;
-
-import java.io.ByteArrayInputStream;
-import java.nio.charset.StandardCharsets;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.security.PublicKey;
-import java.security.Signature;
-import java.security.SignatureException;
-import java.security.cert.Certificate;
-import java.security.cert.CertificateException;
-import java.security.cert.CertificateFactory;
-import java.security.cert.X509Certificate;
-import java.util.Base64;
-import software.amazon.awssdk.annotations.SdkInternalApi;
-import software.amazon.awssdk.services.sns.messagemanager.SnsCertificateException;
-import software.amazon.awssdk.services.sns.messagemanager.SnsMessage;
-import software.amazon.awssdk.services.sns.messagemanager.SnsSignatureValidationException;
-import software.amazon.awssdk.utils.Validate;
-
-/**
- * Internal validator for SNS message signatures.
- *
- * This class handles cryptographic verification of SNS message signatures using AWS certificates.
- * It supports both SignatureVersion1 (SHA1) and SignatureVersion2 (SHA256) as per AWS SNS standards,
- * ensuring that messages are genuinely from Amazon SNS and have not been tampered with during transmission.
- *
- * The validator performs comprehensive signature verification including:
- * Security Features:
- * Thread Safety: This class is thread-safe as all methods are static
- * and do not maintain any mutable state.
- *
- * Usage: This class is intended for internal use by the SNS message manager
- * and should not be used directly by client code. Signature validation is automatically
- * performed during message parsing through {@link DefaultSnsMessageManager}.
- *
- * @see DefaultSnsMessageManager
- * @see CertificateRetriever
- * @see SnsMessage
- */
-@SdkInternalApi
-public final class SignatureValidator {
-
- private static final String SIGNATURE_VERSION_1 = "1";
- private static final String SIGNATURE_VERSION_2 = "2";
-
- private static final String SHA1_WITH_RSA = "SHA1withRSA";
- private static final String SHA256_WITH_RSA = "SHA256withRSA";
-
- private static final String CERTIFICATE_TYPE = "X.509";
-
- private SignatureValidator() {
- // Utility class - prevent instantiation
- }
-
- /**
- * Validates the signature of an SNS message using the provided certificate.
- *
- * This method performs comprehensive cryptographic verification of the SNS message signature
- * to ensure the message is authentic and from Amazon SNS. The validation process includes:
- * The method supports both SignatureVersion1 (SHA1withRSA) and SignatureVersion2 (SHA256withRSA)
- * signature algorithms as specified by AWS SNS standards.
- *
- * Security Validation:
- *
- * This method performs comprehensive validation of the certificate issuer to ensure
- * it comes from a trusted Amazon SNS certificate authority. It checks multiple
- * issuer patterns to support different AWS partitions and certificate structures.
- *
- * @param certificate The certificate to validate.
- * @throws SnsCertificateException If the certificate is not from a trusted Amazon SNS issuer.
- */
- private static void validateCertificateChainOfTrust(X509Certificate certificate) {
- String issuerDN = certificate.getIssuerDN().getName();
- String subjectDN = certificate.getSubjectDN().getName();
-
- if (!isAmazonSnsIssuer(issuerDN)) {
- throw SnsCertificateException.builder()
- .message("Certificate is not issued by Amazon SNS. Issuer: " + issuerDN +
- ". Expected issuer patterns: CN=sns.amazonaws.com, CN=Amazon, " +
- "O=Amazon.com Inc., or O=Amazon Web Services")
- .build();
- }
-
- // Additional validation for subject DN to ensure it's an SNS certificate
- if (!isValidSnsSubject(subjectDN)) {
- throw SnsCertificateException.builder()
- .message("Certificate subject is not valid for Amazon SNS. Subject: " + subjectDN +
- ". Expected subject patterns should contain sns.amazonaws.com or Amazon SNS identifiers")
- .build();
- }
- }
-
- /**
- * Validates certificate key usage to ensure it's appropriate for signature verification.
- *
- * This method checks that the certificate has the appropriate key usage extensions
- * for digital signature verification, which is required for SNS message validation.
- *
- * @param certificate The certificate to validate.
- * @throws SnsCertificateException If the certificate doesn't have appropriate key usage.
- */
- private static void validateCertificateKeyUsage(X509Certificate certificate) {
- boolean[] keyUsage = certificate.getKeyUsage();
-
- // Key usage array indices according to RFC 5280:
- // 0: digitalSignature, 1: nonRepudiation, 2: keyEncipherment, etc.
- if (keyUsage != null && keyUsage.length > 0) {
- // Check if digital signature is enabled (index 0)
- if (!keyUsage[0]) {
- throw SnsCertificateException.builder()
- .message("Certificate does not have digital signature key usage enabled, " +
- "which is required for SNS message signature verification")
- .build();
- }
- }
- // If keyUsage is null, the certificate doesn't restrict key usage, which is acceptable
- }
-
- private static boolean isAmazonSnsIssuer(String issuerDN) {
- if (issuerDN == null) {
- return false;
- }
-
- // Convert to lowercase for case-insensitive matching
- String normalizedIssuer = issuerDN.toLowerCase();
-
- // Check for various Amazon SNS certificate issuer patterns
- return normalizedIssuer.contains("cn=sns.amazonaws.com") ||
- normalizedIssuer.contains("cn=amazon") ||
- normalizedIssuer.contains("o=amazon.com inc.") ||
- normalizedIssuer.contains("o=amazon web services") ||
- normalizedIssuer.contains("o=amazon.com, inc.") ||
- normalizedIssuer.contains("cn=amazon web services") ||
- // Support for different AWS partitions
- normalizedIssuer.contains("amazonaws.com") && normalizedIssuer.contains("amazon");
- }
-
- /**
- * Validates that the certificate subject is appropriate for Amazon SNS.
- *
- * This method checks the certificate subject DN to ensure it contains
- * identifiers that are consistent with Amazon SNS certificates.
- *
- * @param subjectDN The subject DN to validate.
- * @return true if the subject is valid for SNS, false otherwise.
- */
- private static boolean isValidSnsSubject(String subjectDN) {
- if (subjectDN == null) {
- return false;
- }
-
- // Convert to lowercase for case-insensitive matching
- String normalizedSubject = subjectDN.toLowerCase();
-
- // Check for SNS-related subject patterns
- return normalizedSubject.contains("sns.amazonaws.com") ||
- normalizedSubject.contains("amazon") ||
- normalizedSubject.contains("aws") ||
- // Allow certificates that contain amazonaws.com domain
- normalizedSubject.contains("amazonaws.com");
- }
-
- private static String getSignatureAlgorithm(String signatureVersion) {
- switch (signatureVersion) {
- case SIGNATURE_VERSION_1:
- return SHA1_WITH_RSA;
- case SIGNATURE_VERSION_2:
- return SHA256_WITH_RSA;
- default:
- throw SnsSignatureValidationException.builder()
- .message("Unsupported signature version: " + signatureVersion +
- ". Supported versions are: " + SIGNATURE_VERSION_1 + ", " + SIGNATURE_VERSION_2)
- .build();
- }
- }
-
- private static String buildCanonicalMessage(SnsMessage message) {
- StringBuilder canonical = new StringBuilder();
-
- // Build canonical string according to SNS specification
- // The order and format must match exactly what SNS uses for signing
-
- canonical.append("Message\n");
- canonical.append(message.message()).append("\n");
-
- canonical.append("MessageId\n");
- canonical.append(message.messageId()).append("\n");
-
- // Subject is optional but must be included if present
- if (message.subject().isPresent()) {
- canonical.append("Subject\n");
- canonical.append(message.subject().get()).append("\n");
- }
-
- canonical.append("Timestamp\n");
- canonical.append(message.timestamp().toString()).append("\n");
-
- canonical.append("TopicArn\n");
- canonical.append(message.topicArn()).append("\n");
-
- canonical.append("Type\n");
- canonical.append(message.type()).append("\n");
-
- return canonical.toString();
- }
-
- private static void verifySignature(String signatureBase64, String canonicalMessage,
- PublicKey publicKey, String signatureAlgorithm) {
- try {
- // Decode the base64 signature
- byte[] signatureBytes = Base64.getDecoder().decode(signatureBase64);
-
- // Initialize signature verification
- Signature signature = Signature.getInstance(signatureAlgorithm);
- signature.initVerify(publicKey);
- signature.update(canonicalMessage.getBytes(StandardCharsets.UTF_8));
-
- // Verify the signature
- boolean isValid = signature.verify(signatureBytes);
-
- if (!isValid) {
- throw SnsSignatureValidationException.builder()
- .message("Message signature verification failed. The message may have been tampered with or " +
- "is not from Amazon SNS.")
- .build();
- }
-
- } catch (IllegalArgumentException e) {
- throw SnsSignatureValidationException.builder()
- .message("Invalid base64 signature format: " + e.getMessage())
- .cause(e)
- .build();
- } catch (NoSuchAlgorithmException e) {
- throw SnsSignatureValidationException.builder()
- .message("Signature algorithm not supported: " + signatureAlgorithm)
- .cause(e)
- .build();
- } catch (InvalidKeyException e) {
- throw SnsSignatureValidationException.builder()
- .message("Invalid public key for signature verification: " + e.getMessage())
- .cause(e)
- .build();
- } catch (SignatureException e) {
- throw SnsSignatureValidationException.builder()
- .message("Signature verification failed: " + e.getMessage())
- .cause(e)
- .build();
- }
- }
-}
\ No newline at end of file
diff --git a/services/sns/src/main/java/software/amazon/awssdk/services/sns/internal/messagemanager/SnsMessageParser.java b/services/sns/src/main/java/software/amazon/awssdk/services/sns/internal/messagemanager/SnsMessageParser.java
deleted file mode 100644
index bfd522f9cc8e..000000000000
--- a/services/sns/src/main/java/software/amazon/awssdk/services/sns/internal/messagemanager/SnsMessageParser.java
+++ /dev/null
@@ -1,505 +0,0 @@
-/*
- * 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.services.sns.internal.messagemanager;
-
-import java.time.Instant;
-import java.time.format.DateTimeParseException;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-import software.amazon.awssdk.annotations.SdkInternalApi;
-import software.amazon.awssdk.protocols.jsoncore.JsonNode;
-import software.amazon.awssdk.protocols.jsoncore.JsonNodeParser;
-import software.amazon.awssdk.services.sns.messagemanager.SnsMessage;
-import software.amazon.awssdk.services.sns.messagemanager.SnsMessageParsingException;
-import software.amazon.awssdk.utils.StringUtils;
-import software.amazon.awssdk.utils.Validate;
-
-/**
- * Internal parser for SNS message JSON payloads.
- */
-@SdkInternalApi
-public final class SnsMessageParser {
-
- private static final JsonNodeParser JSON_PARSER = JsonNodeParser.create();
-
- // Supported message types
- private static final String TYPE_NOTIFICATION = "Notification";
- private static final String TYPE_SUBSCRIPTION_CONFIRMATION = "SubscriptionConfirmation";
- private static final String TYPE_UNSUBSCRIBE_CONFIRMATION = "UnsubscribeConfirmation";
-
- // Required fields for all message types
- private static final Set
- * This class allows customization of certificate caching behavior, HTTP client settings,
- * and other validation parameters for the SNS message validation process.
- *
- * Example usage:
- *
- * This determines how long certificates are cached before being re-fetched from AWS.
- * A longer timeout reduces HTTP requests but may delay detection of certificate changes.
- *
- * @return The certificate cache timeout (never null).
- */
- public Duration certificateCacheTimeout() {
- return certificateCacheTimeout;
- }
-
- /**
- * Returns the HTTP client to use for certificate retrieval.
- *
- * If not specified, the default SDK HTTP client will be used.
- *
- * @return The HTTP client, or null if the default should be used.
- */
- public SdkHttpClient httpClient() {
- return httpClient;
- }
-
- @Override
- public Builder toBuilder() {
- return new DefaultBuilder(this);
- }
-
- @Override
- public boolean equals(Object obj) {
- if (this == obj) {
- return true;
- }
- if (obj == null || getClass() != obj.getClass()) {
- return false;
- }
- MessageManagerConfiguration that = (MessageManagerConfiguration) obj;
- return Objects.equals(certificateCacheTimeout, that.certificateCacheTimeout) &&
- Objects.equals(httpClient, that.httpClient);
- }
-
- @Override
- public int hashCode() {
- return Objects.hashCode(certificateCacheTimeout) * 31 + Objects.hashCode(httpClient);
- }
-
- @Override
- public String toString() {
- return ToString.builder("MessageManagerConfiguration")
- .add("certificateCacheTimeout", certificateCacheTimeout)
- .add("httpClient", httpClient)
- .build();
- }
-
- /**
- * Builder for creating {@link MessageManagerConfiguration} instances.
- */
- @NotThreadSafe
- public interface Builder extends CopyableBuilder
- * This determines how long certificates are cached before being re-fetched from AWS.
- * Must be positive.
- *
- * @param certificateCacheTimeout The cache timeout duration.
- * @return This builder for method chaining.
- * @throws IllegalArgumentException If the timeout is null or not positive.
- */
- Builder certificateCacheTimeout(Duration certificateCacheTimeout);
-
- /**
- * Sets the HTTP client to use for certificate retrieval.
- *
- * If not specified, the default SDK HTTP client will be used.
- *
- * @param httpClient The HTTP client to use.
- * @return This builder for method chaining.
- */
- Builder httpClient(SdkHttpClient httpClient);
-
- /**
- * Applies a mutation to this builder using the provided consumer.
- *
- * This is a convenience method that allows for fluent configuration using lambda expressions.
- *
- * @param mutator A consumer that applies mutations to this builder.
- * @return This builder for method chaining.
- */
- default Builder applyMutation(java.util.function.Consumer
- * This exception is thrown when there are issues with the certificates used to verify SNS message signatures.
- * Certificate validation is a critical security step that ensures messages are genuinely from Amazon SNS.
- *
- * Common scenarios that trigger this exception:
- *
- * When this exception is thrown, the message should be considered untrusted and should not be processed,
- * as the certificate validation is essential for ensuring message authenticity.
- */
-@SdkPublicApi
-public class SnsCertificateException extends SnsMessageValidationException {
-
- private static final long serialVersionUID = 1L;
-
- /**
- * Constructs a new SnsCertificateException with the specified detail message.
- *
- * @param message The detail message explaining the certificate validation failure.
- */
- public SnsCertificateException(String message) {
- super(message);
- }
-
- /**
- * Constructs a new SnsCertificateException with the specified detail message and cause.
- *
- * @param message The detail message explaining the certificate validation failure.
- * @param cause The underlying cause of the certificate validation failure.
- */
- public SnsCertificateException(String message, Throwable cause) {
- super(message, cause);
- }
-
- /**
- * Creates a new builder for constructing SnsCertificateException instances.
- *
- * @return A new builder instance.
- */
- public static SnsMessageValidationException.Builder builder() {
- return new SnsMessageValidationException.Builder() {
- @Override
- public SnsMessageValidationException build() {
- if (cause != null) {
- return new SnsCertificateException(message, cause);
- }
- return new SnsCertificateException(message);
- }
- };
- }
-}
\ No newline at end of file
diff --git a/services/sns/src/main/java/software/amazon/awssdk/services/sns/messagemanager/SnsMessage.java b/services/sns/src/main/java/software/amazon/awssdk/services/sns/messagemanager/SnsMessage.java
deleted file mode 100644
index 46d113027b1b..000000000000
--- a/services/sns/src/main/java/software/amazon/awssdk/services/sns/messagemanager/SnsMessage.java
+++ /dev/null
@@ -1,459 +0,0 @@
-/*
- * 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.services.sns.messagemanager;
-
-import java.time.Instant;
-import java.util.Collections;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import software.amazon.awssdk.annotations.SdkPublicApi;
-import software.amazon.awssdk.utils.ToString;
-import software.amazon.awssdk.utils.Validate;
-
-/**
- * Represents a validated SNS message with all its attributes.
- *
- * This class provides access to all standard SNS message fields after successful signature validation.
- * The message has been cryptographically verified to be authentic and from Amazon SNS.
- *
- * Supports all SNS message types:
- * This class is immutable and thread-safe. All required fields are validated during construction.
- * Instances are typically created through the {@link SnsMessageManager#parseMessage(String)} method
- * after successful message validation.
- *
- * Example usage:
- *
- * Valid values are:
- * This field is optional and may not be present in all message types.
- * It is commonly used in Notification messages to provide a brief description
- * of the message content.
- *
- * @return An Optional containing the subject, or empty if not present
- */
- public Optional
- * For Notification messages, this contains the actual notification content.
- * For confirmation messages, this may contain confirmation details.
- *
- * @return The message content (never null).
- */
- public String message() {
- return message;
- }
-
- /**
- * Returns the timestamp when the message was published.
- *
- * @return The message timestamp (never null).
- */
- public Instant timestamp() {
- return timestamp;
- }
-
- /**
- * Returns the signature version used to sign the message.
- *
- * Valid values are:
- *
- * This URL has been validated to ensure it comes from a trusted SNS-signed domain.
- *
- * @return The signing certificate URL (never null).
- */
- public String signingCertUrl() {
- return signingCertUrl;
- }
-
- /**
- * Returns the unsubscribe URL, if present.
- *
- * This field is typically present in Notification messages and allows recipients
- * to unsubscribe from the topic.
- *
- * @return An Optional containing the unsubscribe URL, or empty if not present.
- */
- public Optional
- * This field is required for SubscriptionConfirmation and UnsubscribeConfirmation messages.
- *
- * @return An Optional containing the token, or empty if not present.
- */
- public Optional
- * Message attributes are key-value pairs that provide additional metadata about the message.
- *
- * @return A map of message attributes (never null, but may be empty).
- */
- public Map
- * This manager provides automatic validation of SNS message signatures received via HTTP/HTTPS endpoints,
- * ensuring that messages are genuinely from Amazon SNS and have not been tampered with during transmission.
- * It supports both SignatureVersion1 (SHA1) and SignatureVersion2 (SHA256) as per AWS SNS standards.
- *
- * The manager handles certificate retrieval, caching, and validation automatically, supporting different
- * AWS regions and partitions (aws, aws-gov, aws-cn).
- *
- * Example usage:
- *
- * This method reads the JSON message payload, validates the signature using AWS cryptographic verification,
- * and returns a parsed SNS message object with all message attributes if validation succeeds.
- *
- * @param messageStream The InputStream containing the JSON SNS message payload.
- * @return A validated {@link SnsMessage} object containing all message fields.
- * @throws SnsMessageValidationException If the message signature is invalid, the message format is malformed,
- * or contains unexpected fields.
- * @throws NullPointerException If messageStream is null.
- */
- SnsMessage parseMessage(InputStream messageStream);
-
- /**
- * Parses and validates an SNS message from a String.
- *
- * This method parses the JSON message payload, validates the signature using AWS cryptographic verification,
- * and returns a parsed SNS message object with all message attributes if validation succeeds.
- *
- * @param messageContent The String containing the JSON SNS message payload.
- * @return A validated {@link SnsMessage} object containing all message fields.
- * @throws SnsMessageValidationException If the message signature is invalid, the message format is malformed,
- * or contains unexpected fields.
- * @throws NullPointerException If messageContent is null.
- */
- SnsMessage parseMessage(String messageContent);
-
- /**
- * Builder for creating and configuring an {@link SnsMessageManager}.
- */
- interface Builder {
-
- /**
- * Sets the configuration for the message manager.
- *
- * @param configuration The configuration to use.
- * @return This builder for method chaining.
- */
- Builder configuration(MessageManagerConfiguration configuration);
-
- /**
- * Sets the configuration for the message manager using a {@link Consumer} to configure the settings.
- *
- * @param configuration A {@link Consumer} to configure the {@link MessageManagerConfiguration}.
- * @return This builder for method chaining.
- */
- default Builder configuration(Consumer
- * This exception is thrown in the following scenarios:
- *
- * The exception message provides specific details about what parsing error occurred,
- * helping developers identify and fix message format issues.
- */
-@SdkPublicApi
-public class SnsMessageParsingException extends SnsMessageValidationException {
-
- private static final long serialVersionUID = 1L;
-
- /**
- * Constructs a new SnsMessageParsingException with the specified detail message.
- *
- * @param message The detail message explaining the parsing failure.
- */
- public SnsMessageParsingException(String message) {
- super(message);
- }
-
- /**
- * Constructs a new SnsMessageParsingException with the specified detail message and cause.
- *
- * @param message The detail message explaining the parsing failure.
- * @param cause The underlying cause of the parsing failure (e.g., JSON parsing exception).
- */
- public SnsMessageParsingException(String message, Throwable cause) {
- super(message, cause);
- }
-
- /**
- * Creates a new builder for constructing SnsMessageParsingException instances.
- *
- * @return A new builder instance.
- */
- public static SnsMessageValidationException.Builder builder() {
- return new SnsMessageValidationException.Builder() {
- @Override
- public SnsMessageValidationException build() {
- if (cause != null) {
- return new SnsMessageParsingException(message, cause);
- }
- return new SnsMessageParsingException(message);
- }
- };
- }
-}
\ No newline at end of file
diff --git a/services/sns/src/main/java/software/amazon/awssdk/services/sns/messagemanager/SnsMessageValidationException.java b/services/sns/src/main/java/software/amazon/awssdk/services/sns/messagemanager/SnsMessageValidationException.java
deleted file mode 100644
index bc1e83053aa3..000000000000
--- a/services/sns/src/main/java/software/amazon/awssdk/services/sns/messagemanager/SnsMessageValidationException.java
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * 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.services.sns.messagemanager;
-
-import software.amazon.awssdk.annotations.SdkPublicApi;
-
-/**
- * Base exception for all SNS message validation failures.
- *
- * This exception is thrown when SNS message validation fails for any reason, including:
- *
- * Specific subclasses provide more detailed error information for different types of validation failures.
- *
- * @see SnsMessageParsingException
- * @see SnsSignatureValidationException
- * @see SnsCertificateException
- */
-@SdkPublicApi
-public class SnsMessageValidationException extends RuntimeException {
-
- private static final long serialVersionUID = 1L;
-
- /**
- * Constructs a new SnsMessageValidationException with the specified detail message.
- *
- * @param message The detail message explaining the validation failure.
- */
- public SnsMessageValidationException(String message) {
- super(message);
- }
-
- /**
- * Constructs a new SnsMessageValidationException with the specified detail message and cause.
- *
- * @param message The detail message explaining the validation failure.
- * @param cause The underlying cause of the validation failure.
- */
- public SnsMessageValidationException(String message, Throwable cause) {
- super(message, cause);
- }
-
- /**
- * Creates a new builder for constructing SnsMessageValidationException instances.
- *
- * @return A new builder instance.
- */
- public static Builder builder() {
- return new Builder();
- }
-
- /**
- * Builder for creating SnsMessageValidationException instances.
- */
- public static class Builder {
- protected String message;
- protected Throwable cause;
-
- protected Builder() {
- }
-
- /**
- * Sets the detail message for the exception.
- *
- * @param message The detail message.
- * @return This builder for method chaining.
- */
- public Builder message(String message) {
- this.message = message;
- return this;
- }
-
- /**
- * Sets the underlying cause of the exception.
- *
- * @param cause The underlying cause.
- * @return This builder for method chaining.
- */
- public Builder cause(Throwable cause) {
- this.cause = cause;
- return this;
- }
-
- /**
- * Builds a new SnsMessageValidationException instance.
- *
- * @return A new exception with the configured properties.
- */
- public SnsMessageValidationException build() {
- if (cause != null) {
- return new SnsMessageValidationException(message, cause);
- }
- return new SnsMessageValidationException(message);
- }
- }
-}
\ No newline at end of file
diff --git a/services/sns/src/main/java/software/amazon/awssdk/services/sns/messagemanager/SnsSignatureValidationException.java b/services/sns/src/main/java/software/amazon/awssdk/services/sns/messagemanager/SnsSignatureValidationException.java
deleted file mode 100644
index 0eb5b8356e57..000000000000
--- a/services/sns/src/main/java/software/amazon/awssdk/services/sns/messagemanager/SnsSignatureValidationException.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * 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.services.sns.messagemanager;
-
-import software.amazon.awssdk.annotations.SdkPublicApi;
-
-/**
- * Exception thrown when SNS message signature verification fails.
- *
- * This exception is thrown when the cryptographic signature of an SNS message cannot be verified,
- * indicating that the message may not be authentic or may have been tampered with during transmission.
- *
- * Common scenarios that trigger this exception:
- *
- * When this exception is thrown, the message should be considered untrusted and should not be processed.
- */
-@SdkPublicApi
-public class SnsSignatureValidationException extends SnsMessageValidationException {
-
- private static final long serialVersionUID = 1L;
-
- /**
- * Constructs a new SnsSignatureValidationException with the specified detail message.
- *
- * @param message The detail message explaining the signature validation failure.
- */
- public SnsSignatureValidationException(String message) {
- super(message);
- }
-
- /**
- * Constructs a new SnsSignatureValidationException with the specified detail message and cause.
- *
- * @param message The detail message explaining the signature validation failure.
- * @param cause The underlying cause of the signature validation failure.
- */
- public SnsSignatureValidationException(String message, Throwable cause) {
- super(message, cause);
- }
-
- /**
- * Creates a new builder for constructing SnsSignatureValidationException instances.
- *
- * @return A new builder instance.
- */
- public static SnsMessageValidationException.Builder builder() {
- return new SnsMessageValidationException.Builder() {
- @Override
- public SnsMessageValidationException build() {
- if (cause != null) {
- return new SnsSignatureValidationException(message, cause);
- }
- return new SnsSignatureValidationException(message);
- }
- };
- }
-}
\ No newline at end of file
diff --git a/services/sns/src/test/java/software/amazon/awssdk/services/sns/internal/messagemanager/CertificateRetrieverTest.java b/services/sns/src/test/java/software/amazon/awssdk/services/sns/internal/messagemanager/CertificateRetrieverTest.java
deleted file mode 100644
index 3f26d4fbf4e1..000000000000
--- a/services/sns/src/test/java/software/amazon/awssdk/services/sns/internal/messagemanager/CertificateRetrieverTest.java
+++ /dev/null
@@ -1,828 +0,0 @@
-/*
- * 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.services.sns.internal.messagemanager;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.lenient;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.charset.StandardCharsets;
-import java.time.Duration;
-import java.util.Optional;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.ValueSource;
-import software.amazon.awssdk.http.AbortableInputStream;
-import software.amazon.awssdk.http.ExecutableHttpRequest;
-import software.amazon.awssdk.http.HttpExecuteRequest;
-import software.amazon.awssdk.http.HttpExecuteResponse;
-import software.amazon.awssdk.http.SdkHttpClient;
-import software.amazon.awssdk.http.SdkHttpFullResponse;
-import software.amazon.awssdk.http.SdkHttpResponse;
-import software.amazon.awssdk.services.sns.messagemanager.SnsCertificateException;
-
-/**
- * Unit tests for {@link CertificateRetriever}.
- *
- * This test class validates the certificate retrieval and caching functionality
- * of the SNS message manager. It focuses on testing security validations, caching behavior,
- * error handling, and thread-safety.
- *
- * The test strategy includes:
- * Initializes a mock HTTP client and creates a CertificateRetriever instance
- * with a 5-minute cache timeout for testing.
- */
- @BeforeEach
- void setUp() {
- mockHttpClient = mock(SdkHttpClient.class);
- certificateRetriever = new CertificateRetriever(mockHttpClient, Duration.ofMinutes(5));
- }
-
-
-
- // ========== Constructor Validation Tests ==========
-
- /**
- * Tests that CertificateRetriever constructor properly validates null HTTP client parameter.
- *
- * This test ensures that the constructor performs proper null checking on the httpClient
- * parameter and throws a {@link NullPointerException} with a descriptive error message
- * when null is provided.
- *
- * This validation is critical for preventing null pointer exceptions during certificate
- * retrieval operations and ensuring that callers receive clear feedback about invalid parameters.
- *
- * @throws NullPointerException Expected exception when httpClient parameter is null
- */
- @Test
- void constructor_nullHttpClient_throwsException() {
- assertThatThrownBy(() -> new CertificateRetriever(null, Duration.ofMinutes(5)))
- .isInstanceOf(NullPointerException.class)
- .hasMessageContaining("httpClient must not be null");
- }
-
- /**
- * Tests that CertificateRetriever constructor properly validates null cache timeout parameter.
- *
- * This test ensures proper null checking on the certificateCacheTimeout parameter and verifies
- * that a {@link NullPointerException} is thrown with a descriptive error message when null is provided.
- *
- * The cache timeout is essential for controlling certificate cache behavior and preventing
- * indefinite caching of potentially compromised certificates.
- *
- * @throws NullPointerException Expected exception when certificateCacheTimeout parameter is null
- */
- @Test
- void constructor_nullCacheTimeout_throwsException() {
- assertThatThrownBy(() -> new CertificateRetriever(mockHttpClient, null))
- .isInstanceOf(NullPointerException.class)
- .hasMessageContaining("certificateCacheTimeout must not be null");
- }
-
- // ========== Certificate URL Validation Tests ==========
-
- /**
- * Tests that certificate retrieval properly validates null URL parameter.
- *
- * This test ensures that the {@link CertificateRetriever#retrieveCertificate(String)}
- * method performs proper null checking on the certificateUrl parameter and throws a
- * {@link NullPointerException} with a descriptive error message when null is provided.
- *
- * This validation is critical for preventing null pointer exceptions during URL
- * processing and ensuring that callers receive clear feedback about invalid parameters.
- *
- * @throws NullPointerException Expected exception when certificateUrl parameter is null
- */
- @Test
- void retrieveCertificate_nullUrl_throwsException() {
- assertThatThrownBy(() -> certificateRetriever.retrieveCertificate(null))
- .isInstanceOf(NullPointerException.class)
- .hasMessageContaining("certificateUrl must not be null");
- }
-
- /**
- * Tests that certificate retrieval rejects empty URL strings.
- *
- * This test verifies that empty strings are properly detected and rejected
- * with an appropriate {@link SnsCertificateException}. Empty URLs cannot be
- * processed for certificate retrieval.
- *
- * @throws SnsCertificateException Expected exception when URL is empty
- */
- @Test
- void retrieveCertificate_emptyUrl_throwsException() {
- assertThatThrownBy(() -> certificateRetriever.retrieveCertificate(""))
- .isInstanceOf(SnsCertificateException.class)
- .hasMessageContaining("Certificate URL cannot be null or empty");
- }
-
- /**
- * Tests that certificate retrieval rejects blank URL strings (whitespace only).
- *
- * This test verifies that URLs containing only whitespace characters are
- * properly detected and rejected. Such URLs are effectively empty and cannot
- * be used for certificate retrieval.
- *
- * @throws SnsCertificateException Expected exception when URL contains only whitespace
- */
- @Test
- void retrieveCertificate_blankUrl_throwsException() {
- assertThatThrownBy(() -> certificateRetriever.retrieveCertificate(" "))
- .isInstanceOf(SnsCertificateException.class)
- .hasMessageContaining("Certificate URL cannot be null or empty");
- }
-
- /**
- * Tests that certificate retrieval rejects malformed URLs.
- *
- * This test verifies that URLs that don't conform to valid URL syntax
- * are properly detected and rejected with an appropriate error message.
- * This prevents attempts to retrieve certificates from invalid locations.
- *
- * @throws SnsCertificateException Expected exception when URL format is invalid
- */
- @Test
- void retrieveCertificate_invalidUrlFormat_throwsException() {
- assertThatThrownBy(() -> certificateRetriever.retrieveCertificate("not-a-valid-url"))
- .isInstanceOf(SnsCertificateException.class)
- .hasMessageContaining("Certificate URL must use HTTPS");
- }
-
- /**
- * Tests that certificate retrieval enforces HTTPS-only policy.
- *
- * This test verifies that HTTP URLs are rejected to ensure certificate
- * retrieval only occurs over secure connections. This is a critical security
- * requirement to prevent man-in-the-middle attacks on certificate retrieval.
- *
- * @throws SnsCertificateException Expected exception when URL uses HTTP instead of HTTPS
- */
- @Test
- void retrieveCertificate_httpUrl_throwsException() {
- assertThatThrownBy(() -> certificateRetriever.retrieveCertificate("http://sns.us-east-1.amazonaws.com/cert.pem"))
- .isInstanceOf(SnsCertificateException.class)
- .hasMessageContaining("Certificate URL must use HTTPS");
- }
-
- /**
- * Tests that certificate retrieval rejects URLs from untrusted domains.
- *
- * This test verifies that only URLs from trusted SNS domains are accepted
- * for certificate retrieval. This prevents attackers from providing certificates
- * from malicious domains that could be used to forge SNS messages.
- *
- * @throws SnsCertificateException Expected exception when URL is from untrusted domain
- */
- @Test
- void retrieveCertificate_untrustedDomain_throwsException() {
- assertThatThrownBy(() -> certificateRetriever.retrieveCertificate("https://malicious.com/cert.pem"))
- .isInstanceOf(SnsCertificateException.class)
- .hasMessageContaining("Certificate URL is not from a trusted SNS domain");
- }
-
- /**
- * Tests that certificate retrieval accepts URLs from all trusted SNS domains.
- *
- * This parameterized test verifies that certificate URLs from legitimate SNS domains
- * across different AWS partitions are accepted for certificate retrieval. The test
- * covers standard AWS regions, GovCloud regions, and China regions.
- *
- * Trusted domains include:
- * This parameterized test verifies that certificate URLs that appear similar to
- * legitimate SNS domains but are actually malicious or malformed are properly rejected.
- * This includes subdomain spoofing, domain spoofing, and malformed domain patterns.
- *
- * The test covers various attack vectors:
- * This test verifies that when the HTTP request for certificate retrieval
- * returns an error status code (such as 404 Not Found), the retriever throws
- * an appropriate {@link SnsCertificateException} with details about the HTTP error.
- *
- * This ensures that network-level failures are properly reported to callers
- * with sufficient context for debugging and error handling.
- *
- * @throws SnsCertificateException Expected exception when HTTP request fails
- */
- @Test
- void retrieveCertificate_httpError_throwsException() throws Exception {
- // Setup HTTP error response
- SdkHttpResponse errorResponse = SdkHttpResponse.builder()
- .statusCode(404)
- .build();
-
- HttpExecuteResponse httpResponse = mock(HttpExecuteResponse.class);
- when(httpResponse.httpResponse()).thenReturn(errorResponse);
-
- ExecutableHttpRequest executableRequest = mock(ExecutableHttpRequest.class);
- when(executableRequest.call()).thenReturn(httpResponse);
- when(mockHttpClient.prepareRequest(any(HttpExecuteRequest.class))).thenReturn(executableRequest);
-
- assertThatThrownBy(() -> certificateRetriever.retrieveCertificate(VALID_CERT_URL_US_EAST_1))
- .isInstanceOf(SnsCertificateException.class)
- .hasMessageContaining("Unexpected error while retrieving certificate")
- .hasCauseInstanceOf(SnsCertificateException.class);
- }
-
- @Test
- void retrieveCertificate_ioException_throwsException() throws Exception {
- ExecutableHttpRequest executableRequest = mock(ExecutableHttpRequest.class);
- when(executableRequest.call()).thenThrow(new IOException("Network error"));
- when(mockHttpClient.prepareRequest(any(HttpExecuteRequest.class))).thenReturn(executableRequest);
-
- assertThatThrownBy(() -> certificateRetriever.retrieveCertificate(VALID_CERT_URL_US_EAST_1))
- .isInstanceOf(SnsCertificateException.class)
- .hasMessageContaining("IO error while retrieving certificate")
- .hasCauseInstanceOf(IOException.class);
- }
-
- @Test
- void retrieveCertificate_unexpectedException_throwsException() throws Exception {
- ExecutableHttpRequest executableRequest = mock(ExecutableHttpRequest.class);
- when(executableRequest.call()).thenThrow(new RuntimeException("Unexpected error"));
- when(mockHttpClient.prepareRequest(any(HttpExecuteRequest.class))).thenReturn(executableRequest);
-
- assertThatThrownBy(() -> certificateRetriever.retrieveCertificate(VALID_CERT_URL_US_EAST_1))
- .isInstanceOf(SnsCertificateException.class)
- .hasMessageContaining("Unexpected error while retrieving certificate")
- .hasCauseInstanceOf(RuntimeException.class);
- }
-
- @Test
- void retrieveCertificate_emptyResponseBody_throwsException() throws Exception {
- SdkHttpResponse successResponse = SdkHttpResponse.builder()
- .statusCode(200)
- .build();
-
- HttpExecuteResponse httpResponse = mock(HttpExecuteResponse.class);
- when(httpResponse.httpResponse()).thenReturn(successResponse);
- when(httpResponse.responseBody()).thenReturn(Optional.empty());
-
- ExecutableHttpRequest executableRequest = mock(ExecutableHttpRequest.class);
- when(executableRequest.call()).thenReturn(httpResponse);
- when(mockHttpClient.prepareRequest(any(HttpExecuteRequest.class))).thenReturn(executableRequest);
-
- assertThatThrownBy(() -> certificateRetriever.retrieveCertificate(VALID_CERT_URL_US_EAST_1))
- .isInstanceOf(SnsCertificateException.class)
- .hasMessageContaining("Unexpected error while retrieving certificate");
- }
-
- @Test
- void retrieveCertificate_emptyCertificate_throwsException() throws Exception {
- setupSuccessfulHttpResponse(new byte[0]);
-
- assertThatThrownBy(() -> certificateRetriever.retrieveCertificate(VALID_CERT_URL_US_EAST_1))
- .isInstanceOf(SnsCertificateException.class)
- .hasMessageContaining("Unexpected error while retrieving certificate");
- }
-
- // ========== Certificate Content Validation Tests ==========
-
- /**
- * Tests that certificate retrieval rejects certificates that are too small to be valid.
- *
- * This test verifies that certificates smaller than the minimum expected size
- * for a valid X.509 certificate are rejected. This helps prevent processing of
- * malformed or truncated certificate data that could cause parsing errors.
- *
- * Valid X.509 certificates, even minimal ones, should be at least 100 bytes
- * due to the required ASN.1 structure and metadata.
- *
- * @throws SnsCertificateException Expected exception when certificate is too small
- */
- @Test
- void retrieveCertificate_tooSmallCertificate_throwsException() throws Exception {
- byte[] tooSmallCert = "small".getBytes(StandardCharsets.UTF_8);
- setupSuccessfulHttpResponse(tooSmallCert);
-
- assertThatThrownBy(() -> certificateRetriever.retrieveCertificate(VALID_CERT_URL_US_EAST_1))
- .isInstanceOf(SnsCertificateException.class)
- .hasMessageContaining("Unexpected error while retrieving certificate")
- .hasCauseInstanceOf(SnsCertificateException.class);
- }
-
- @Test
- void retrieveCertificate_oversizedCertificate_throwsException() throws Exception {
- // Create a certificate larger than 10KB
- byte[] oversizedCert = new byte[11 * 1024];
- // Fill with valid PEM header to pass format validation
- String pemHeader = "-----BEGIN CERTIFICATE-----\n";
- System.arraycopy(pemHeader.getBytes(StandardCharsets.UTF_8), 0, oversizedCert, 0, pemHeader.length());
-
- setupSuccessfulHttpResponse(oversizedCert);
-
- assertThatThrownBy(() -> certificateRetriever.retrieveCertificate(VALID_CERT_URL_US_EAST_1))
- .isInstanceOf(SnsCertificateException.class)
- .hasMessageContaining("Unexpected error while retrieving certificate")
- .hasCauseInstanceOf(SnsCertificateException.class);
- }
-
- @Test
- void retrieveCertificate_invalidCertificateFormat_throwsException() throws Exception {
- byte[] invalidCert = "This is not a valid certificate format".getBytes(StandardCharsets.UTF_8);
- // Make it large enough to pass size validation
- byte[] paddedInvalidCert = new byte[200];
- System.arraycopy(invalidCert, 0, paddedInvalidCert, 0, invalidCert.length);
-
- setupSuccessfulHttpResponse(paddedInvalidCert);
-
- assertThatThrownBy(() -> certificateRetriever.retrieveCertificate(VALID_CERT_URL_US_EAST_1))
- .isInstanceOf(SnsCertificateException.class)
- .hasMessageContaining("Unexpected error while retrieving certificate")
- .hasCauseInstanceOf(SnsCertificateException.class);
- }
-
- @Test
- void retrieveCertificate_certificateWithExcessiveNullBytes_throwsException() throws Exception {
- // Create certificate with too many null bytes (over 10% of content)
- byte[] certWithNulls = new byte[1000];
- String pemHeader = "-----BEGIN CERTIFICATE-----\n";
- System.arraycopy(pemHeader.getBytes(StandardCharsets.UTF_8), 0, certWithNulls, 0, pemHeader.length());
- // Fill rest with null bytes (over 10% threshold)
-
- setupSuccessfulHttpResponse(certWithNulls);
-
- assertThatThrownBy(() -> certificateRetriever.retrieveCertificate(VALID_CERT_URL_US_EAST_1))
- .isInstanceOf(SnsCertificateException.class)
- .hasMessageContaining("Unexpected error while retrieving certificate")
- .hasCauseInstanceOf(SnsCertificateException.class);
- }
-
- @Test
- void retrieveCertificate_certificateWithConsecutiveNullBytes_throwsException() throws Exception {
- // Create certificate with too many consecutive null bytes
- byte[] certWithConsecutiveNulls = new byte[200];
- String pemHeader = "-----BEGIN CERTIFICATE-----\n";
- System.arraycopy(pemHeader.getBytes(StandardCharsets.UTF_8), 0, certWithConsecutiveNulls, 0, pemHeader.length());
- // Add 51 consecutive null bytes starting after the header
- for (int i = pemHeader.length(); i < pemHeader.length() + 51; i++) {
- certWithConsecutiveNulls[i] = 0;
- }
- // Fill rest with non-null data
- for (int i = pemHeader.length() + 51; i < certWithConsecutiveNulls.length; i++) {
- certWithConsecutiveNulls[i] = 'A';
- }
-
- setupSuccessfulHttpResponse(certWithConsecutiveNulls);
-
- assertThatThrownBy(() -> certificateRetriever.retrieveCertificate(VALID_CERT_URL_US_EAST_1))
- .isInstanceOf(SnsCertificateException.class)
- .hasMessageContaining("Unexpected error while retrieving certificate")
- .hasCauseInstanceOf(SnsCertificateException.class);
- }
-
- @Test
- void retrieveCertificate_validPemCertificate_succeeds() throws Exception {
- setupSuccessfulHttpResponse(VALID_PEM_CERTIFICATE.getBytes(StandardCharsets.UTF_8));
-
- byte[] result = certificateRetriever.retrieveCertificate(VALID_CERT_URL_US_EAST_1);
-
- assertThat(result).isNotNull();
- assertThat(new String(result, StandardCharsets.UTF_8)).isEqualTo(VALID_PEM_CERTIFICATE);
- }
-
- @Test
- void retrieveCertificate_validDerCertificate_succeeds() throws Exception {
- setupSuccessfulHttpResponse(VALID_DER_CERTIFICATE);
-
- byte[] result = certificateRetriever.retrieveCertificate(VALID_CERT_URL_US_EAST_1);
-
- assertThat(result).isNotNull();
- assertThat(result).isEqualTo(VALID_DER_CERTIFICATE);
- }
- // ========== Certificate Caching Functionality Tests ==========
-
- /**
- * Tests that certificate caching works correctly for repeated requests to the same URL.
- *
- * This test verifies that when the same certificate URL is requested multiple times,
- * the certificate is retrieved from the HTTP endpoint only once and subsequent requests
- * are served from the cache. This improves performance and reduces network traffic.
- *
- * The test confirms:
- * This helper method configures the mock HTTP client to return a successful
- * HTTP 200 response with the provided certificate bytes as the response body.
- * This allows tests to focus on certificate processing logic without dealing
- * with actual HTTP communication.
- *
- * The mock setup includes:
- * This test class validates the cryptographic signature verification functionality
- * of the SNS message manager. It focuses on testing error conditions, input validation,
- * certificate validation, and exception handling for both SHA1 and SHA256 signature algorithms.
- *
- * The test strategy includes:
- * Due to the complexity of creating valid cryptographic test data, most tests focus
- * on error paths and validation logic rather than full end-to-end cryptographic verification.
- * This approach effectively tests the validation components while avoiding the complexity
- * of generating valid cryptographic signatures and certificates.
- *
- * @see SignatureValidator
- * @see SnsCertificateException
- * @see SnsSignatureValidationException
- */
-class SignatureValidatorTest {
-
- // ========== Input Validation Tests ==========
-
- /**
- * Tests that signature validation properly validates null message parameter.
- *
- * This test ensures that the {@link SignatureValidator#validateSignature(SnsMessage, byte[])}
- * method performs proper null checking on the message parameter and throws a
- * {@link NullPointerException} with a descriptive error message when null is provided.
- *
- * This validation is critical for preventing null pointer exceptions during
- * signature verification and ensuring that callers receive clear feedback about
- * invalid parameters.
- *
- * @throws NullPointerException Expected exception when message parameter is null
- */
- @Test
- void validateSignature_nullMessage_throwsException() {
- assertThatThrownBy(() -> SignatureValidator.validateSignature(null, createInvalidCertificateBytes()))
- .isInstanceOf(NullPointerException.class)
- .hasMessageContaining("message must not be null");
- }
-
- /**
- * Tests that signature validation properly validates null certificate bytes parameter.
- *
- * This test ensures proper null checking on the certificateBytes parameter and verifies
- * that a {@link NullPointerException} is thrown with a descriptive error message.
- *
- * @throws NullPointerException Expected exception when certificateBytes parameter is null
- */
- @Test
- void validateSignature_nullCertificateBytes_throwsException() {
- SnsMessage message = createTestMessage("1");
-
- assertThatThrownBy(() -> SignatureValidator.validateSignature(message, null))
- .isInstanceOf(NullPointerException.class)
- .hasMessageContaining("certificateBytes must not be null");
- }
-
- // ========== Certificate Parsing Tests ==========
-
- /**
- * Tests that signature validation throws appropriate exception when provided with invalid certificate data.
- *
- * This test verifies that the {@link SignatureValidator#validateSignature(SnsMessage, byte[])}
- * method properly handles malformed certificate data by throwing a {@link SnsCertificateException}
- * with an appropriate error message.
- *
- * The test uses intentionally invalid certificate bytes (plain text instead of X.509 format)
- * to trigger certificate parsing failure and verify proper error handling.
- *
- * @throws SnsCertificateException Expected exception when certificate parsing fails
- */
- @Test
- void validateSignature_invalidCertificateFormat_throwsException() {
- SnsMessage message = createTestMessage("1");
- byte[] invalidCertificate = "invalid certificate data".getBytes(StandardCharsets.UTF_8);
-
- assertThatThrownBy(() -> SignatureValidator.validateSignature(message, invalidCertificate))
- .isInstanceOf(SnsCertificateException.class)
- .hasMessageContaining("Failed to parse certificate");
- }
-
- /**
- * Tests that signature validation rejects certificates that are not in valid X.509 format.
- *
- * This test verifies that the validator properly handles certificate data that appears
- * to be in PEM format (with BEGIN/END markers) but contains invalid certificate content.
- * The validator should detect that the certificate is not a valid X.509 certificate
- * and throw an appropriate exception.
- *
- * This test is important for security as it ensures that malformed or spoofed
- * certificates are rejected during the parsing phase, preventing potential
- * security vulnerabilities.
- *
- * @throws SnsCertificateException Expected exception when certificate is not valid X.509 format
- */
- @Test
- void validateSignature_nonX509Certificate_throwsException() {
- SnsMessage message = createTestMessage("1");
- byte[] nonX509Certificate = "-----BEGIN CERTIFICATE-----\nNot a real certificate\n-----END CERTIFICATE-----"
- .getBytes(StandardCharsets.UTF_8);
-
- assertThatThrownBy(() -> SignatureValidator.validateSignature(message, nonX509Certificate))
- .isInstanceOf(SnsCertificateException.class)
- .hasMessageContaining("Failed to parse certificate");
- }
-
- /**
- * Tests certificate parsing failure with empty certificate data.
- *
- * This test verifies that empty certificate bytes are properly handled
- * and result in an appropriate parsing exception.
- *
- * @throws SnsCertificateException Expected exception when certificate data is empty
- */
- @Test
- void validateSignature_emptyCertificateBytes_throwsException() {
- SnsMessage message = createTestMessage("1");
- byte[] emptyCertificate = new byte[0];
-
- assertThatThrownBy(() -> SignatureValidator.validateSignature(message, emptyCertificate))
- .isInstanceOf(SnsCertificateException.class)
- .hasMessageContaining("Failed to parse certificate");
- }
-
- // ========== Certificate Chain of Trust Validation Tests ==========
-
- /**
- * Tests certificate validation with various invalid certificate formats.
- *
- * Due to the complexity of creating valid cryptographic test data, these tests focus
- * on certificate parsing and validation error handling rather than full cryptographic verification.
- * This approach effectively tests the validation components while avoiding the complexity
- * of generating valid cryptographic signatures and certificates.
- *
- * @throws SnsCertificateException Expected exception when certificate validation fails
- */
- @Test
- void validateSignature_certificateValidationFailures_throwsException() {
- SnsMessage message = createTestMessage("1");
-
- // Test with various invalid certificate formats that will trigger different validation failures
- byte[][] invalidCertificates = {
- createInvalidCertificateBytes(),
- createMalformedPemCertificate(),
- createEmptyCertificate()
- };
-
- for (byte[] invalidCert : invalidCertificates) {
- assertThatThrownBy(() -> SignatureValidator.validateSignature(message, invalidCert))
- .isInstanceOf(SnsCertificateException.class)
- .hasMessageContaining("Failed to parse certificate");
- }
- }
-
- // ========== Signature Algorithm Tests ==========
-
- /**
- * Tests signature validation with both supported signature versions.
- *
- * This parameterized test verifies that both SignatureVersion1 (SHA1) and
- * SignatureVersion2 (SHA256) are properly handled by the signature algorithm
- * selection logic. Since we're using invalid certificates, we expect certificate
- * validation to fail, but this confirms the signature version parsing works.
- *
- * @param signatureVersion The signature version to test ("1" for SHA1, "2" for SHA256)
- * @throws SnsCertificateException Expected exception due to invalid certificate
- */
- @ParameterizedTest
- @ValueSource(strings = {"1", "2"})
- void validateSignature_supportedSignatureVersions_certificateValidationFails(String signatureVersion) {
- SnsMessage message = createTestMessage(signatureVersion);
- byte[] invalidCertificate = createInvalidCertificateBytes();
-
- assertThatThrownBy(() -> SignatureValidator.validateSignature(message, invalidCertificate))
- .isInstanceOf(SnsCertificateException.class)
- .hasMessageContaining("Failed to parse certificate");
- }
-
- /**
- * Tests signature validation with an unsupported signature version.
- *
- * This test verifies the behavior when an SNS message contains an unsupported
- * signature version (e.g., version "3" when only versions "1" and "2" are supported).
- *
- * Note: In the current implementation, certificate parsing occurs before signature
- * version validation, so this test expects a certificate parsing exception rather than
- * a signature version exception. This reflects the actual order of validation operations
- * in the {@link SignatureValidator}.
- *
- * @throws SnsCertificateException Expected exception due to certificate parsing failure
- * occurring before signature version validation
- */
- @Test
- void validateSignature_unsupportedSignatureVersion_throwsException() {
- SnsMessage message = createTestMessage("3");
- byte[] invalidCertificate = createInvalidCertificateBytes();
-
- assertThatThrownBy(() -> SignatureValidator.validateSignature(message, invalidCertificate))
- .isInstanceOf(SnsCertificateException.class)
- .hasMessageContaining("Failed to parse certificate");
- }
-
- // ========== Signature Verification Tests ==========
-
- /**
- * Tests signature verification with an invalid base64 signature.
- *
- * This test uses an invalid base64 signature to verify that signature decoding
- * validation works properly. Since certificate parsing occurs first, we expect
- * a certificate parsing exception.
- *
- * @throws SnsCertificateException Expected exception due to certificate parsing failure
- */
- @Test
- void validateSignature_invalidBase64Signature_throwsException() {
- SnsMessage message = createTestMessageWithInvalidSignature("1");
- byte[] invalidCertificate = createInvalidCertificateBytes();
-
- assertThatThrownBy(() -> SignatureValidator.validateSignature(message, invalidCertificate))
- .isInstanceOf(SnsCertificateException.class)
- .hasMessageContaining("Failed to parse certificate");
- }
-
- /**
- * Tests signature verification with various signature formats.
- *
- * This test verifies that different signature formats are handled appropriately.
- * Since we're using invalid certificates, we expect certificate validation to fail,
- * but this confirms the signature processing logic is reached.
- *
- * @throws SnsCertificateException Expected exception due to certificate parsing failure
- */
- @Test
- void validateSignature_variousSignatureFormats_throwsException() {
- byte[] invalidCertificate = createInvalidCertificateBytes();
-
- // Test with different signature formats
- SnsMessage[] messages = {
- createTestMessageWithWrongSignature("1"),
- createTestMessageWithInvalidSignature("2"),
- createTestMessage("1"),
- createTestMessage("2")
- };
-
- for (SnsMessage message : messages) {
- assertThatThrownBy(() -> SignatureValidator.validateSignature(message, invalidCertificate))
- .isInstanceOf(SnsCertificateException.class)
- .hasMessageContaining("Failed to parse certificate");
- }
- }
-
- // ========== Test Helper Methods ==========
-
- /**
- * Creates invalid certificate bytes for testing certificate parsing failures.
- *
- * @return Invalid certificate bytes that will cause parsing to fail
- */
- private byte[] createInvalidCertificateBytes() {
- return "invalid certificate for testing".getBytes(StandardCharsets.UTF_8);
- }
-
- /**
- * Creates a test SNS message with the specified signature version.
- *
- * @param signatureVersion The signature version to use ("1", "2", etc.)
- * @return A test SnsMessage with all required fields
- */
- private SnsMessage createTestMessage(String signatureVersion) {
- return SnsMessage.builder()
- .type("Notification")
- .messageId("12345678-1234-1234-1234-123456789012")
- .topicArn("arn:aws:sns:us-east-1:123456789012:MyTopic")
- .message("Test message content")
- .timestamp(Instant.parse("2023-01-01T12:00:00.000Z"))
- .signatureVersion(signatureVersion)
- .signature("dGVzdCBzaWduYXR1cmU=") // "test signature" in base64
- .signingCertUrl("https://sns.us-east-1.amazonaws.com/cert.pem")
- .build();
- }
-
- /**
- * Creates a test SNS message with an invalid base64 signature.
- *
- * @param signatureVersion The signature version to use
- * @return A test SnsMessage with an invalid signature format
- */
- private SnsMessage createTestMessageWithInvalidSignature(String signatureVersion) {
- return SnsMessage.builder()
- .type("Notification")
- .messageId("12345678-1234-1234-1234-123456789012")
- .topicArn("arn:aws:sns:us-east-1:123456789012:MyTopic")
- .message("Test message content")
- .timestamp(Instant.parse("2023-01-01T12:00:00.000Z"))
- .signatureVersion(signatureVersion)
- .signature("invalid-base64-signature!@#$%") // Invalid base64
- .signingCertUrl("https://sns.us-east-1.amazonaws.com/cert.pem")
- .build();
- }
-
- /**
- * Creates a test SNS message with a valid base64 signature that doesn't match the content.
- *
- * @param signatureVersion The signature version to use
- * @return A test SnsMessage with a wrong but valid base64 signature
- */
- private SnsMessage createTestMessageWithWrongSignature(String signatureVersion) {
- return SnsMessage.builder()
- .type("Notification")
- .messageId("12345678-1234-1234-1234-123456789012")
- .topicArn("arn:aws:sns:us-east-1:123456789012:MyTopic")
- .message("Test message content")
- .timestamp(Instant.parse("2023-01-01T12:00:00.000Z"))
- .signatureVersion(signatureVersion)
- .signature("d3Jvbmcgc2lnbmF0dXJl") // "wrong signature" in base64
- .signingCertUrl("https://sns.us-east-1.amazonaws.com/cert.pem")
- .build();
- }
-
- /**
- * Creates a malformed PEM certificate for testing certificate parsing failures.
- *
- * @return Malformed PEM certificate bytes
- */
- private byte[] createMalformedPemCertificate() {
- return "-----BEGIN CERTIFICATE-----\nMalformed certificate content\n-----END CERTIFICATE-----"
- .getBytes(StandardCharsets.UTF_8);
- }
-
- /**
- * Creates empty certificate bytes for testing certificate parsing failures.
- *
- * @return Empty certificate bytes
- */
- private byte[] createEmptyCertificate() {
- return new byte[0];
- }
-}
\ No newline at end of file
diff --git a/services/sns/src/test/java/software/amazon/awssdk/services/sns/internal/messagemanager/SnsMessageParserTest.java b/services/sns/src/test/java/software/amazon/awssdk/services/sns/internal/messagemanager/SnsMessageParserTest.java
deleted file mode 100644
index 8bd7b92a5581..000000000000
--- a/services/sns/src/test/java/software/amazon/awssdk/services/sns/internal/messagemanager/SnsMessageParserTest.java
+++ /dev/null
@@ -1,689 +0,0 @@
-/*
- * 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.services.sns.internal.messagemanager;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-
-import java.time.Instant;
-import java.util.HashMap;
-import java.util.Map;
-import org.junit.jupiter.api.Test;
-import software.amazon.awssdk.services.sns.messagemanager.SnsMessage;
-import software.amazon.awssdk.services.sns.messagemanager.SnsMessageParsingException;
-
-/**
- * Unit tests for {@link SnsMessageParser}.
- */
-class SnsMessageParserTest {
-
- private static final String VALID_NOTIFICATION_JSON = "{"
- + "\"Type\":\"Notification\","
- + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\","
- + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\","
- + "\"Subject\":\"Test Subject\","
- + "\"Message\":\"Test message content\","
- + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\","
- + "\"SignatureVersion\":\"1\","
- + "\"Signature\":\"test-signature\","
- + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\","
- + "\"UnsubscribeURL\":\"https://sns.us-east-1.amazonaws.com/unsubscribe\""
- + "}";
-
- private static final String VALID_SUBSCRIPTION_CONFIRMATION_JSON = "{"
- + "\"Type\":\"SubscriptionConfirmation\","
- + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\","
- + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\","
- + "\"Message\":\"You have chosen to subscribe to the topic\","
- + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\","
- + "\"SignatureVersion\":\"2\","
- + "\"Signature\":\"test-signature\","
- + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\","
- + "\"Token\":\"confirmation-token-12345\""
- + "}";
-
- private static final String VALID_UNSUBSCRIBE_CONFIRMATION_JSON = "{"
- + "\"Type\":\"UnsubscribeConfirmation\","
- + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\","
- + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\","
- + "\"Message\":\"You have been unsubscribed from the topic\","
- + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\","
- + "\"SignatureVersion\":\"1\","
- + "\"Signature\":\"test-signature\","
- + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\","
- + "\"Token\":\"unsubscribe-token-12345\""
- + "}";
-
- @Test
- void parseMessage_validNotificationMessage_parsesSuccessfully() {
- SnsMessage message = SnsMessageParser.parseMessage(VALID_NOTIFICATION_JSON);
-
- assertThat(message.type()).isEqualTo("Notification");
- assertThat(message.messageId()).isEqualTo("12345678-1234-1234-1234-123456789012");
- assertThat(message.topicArn()).isEqualTo("arn:aws:sns:us-east-1:123456789012:MyTopic");
- assertThat(message.subject()).hasValue("Test Subject");
- assertThat(message.message()).isEqualTo("Test message content");
- assertThat(message.timestamp()).isEqualTo(Instant.parse("2023-01-01T12:00:00.000Z"));
- assertThat(message.signatureVersion()).isEqualTo("1");
- assertThat(message.signature()).isEqualTo("test-signature");
- assertThat(message.signingCertUrl()).isEqualTo("https://sns.us-east-1.amazonaws.com/cert.pem");
- assertThat(message.unsubscribeUrl()).hasValue("https://sns.us-east-1.amazonaws.com/unsubscribe");
- assertThat(message.token()).isEmpty();
- assertThat(message.messageAttributes()).isEmpty();
- }
-
- @Test
- void parseMessage_validSubscriptionConfirmationMessage_parsesSuccessfully() {
- SnsMessage message = SnsMessageParser.parseMessage(VALID_SUBSCRIPTION_CONFIRMATION_JSON);
-
- assertThat(message.type()).isEqualTo("SubscriptionConfirmation");
- assertThat(message.messageId()).isEqualTo("12345678-1234-1234-1234-123456789012");
- assertThat(message.topicArn()).isEqualTo("arn:aws:sns:us-east-1:123456789012:MyTopic");
- assertThat(message.subject()).isEmpty();
- assertThat(message.message()).isEqualTo("You have chosen to subscribe to the topic");
- assertThat(message.timestamp()).isEqualTo(Instant.parse("2023-01-01T12:00:00.000Z"));
- assertThat(message.signatureVersion()).isEqualTo("2");
- assertThat(message.signature()).isEqualTo("test-signature");
- assertThat(message.signingCertUrl()).isEqualTo("https://sns.us-east-1.amazonaws.com/cert.pem");
- assertThat(message.unsubscribeUrl()).isEmpty();
- assertThat(message.token()).hasValue("confirmation-token-12345");
- assertThat(message.messageAttributes()).isEmpty();
- }
-
- @Test
- void parseMessage_validUnsubscribeConfirmationMessage_parsesSuccessfully() {
- SnsMessage message = SnsMessageParser.parseMessage(VALID_UNSUBSCRIBE_CONFIRMATION_JSON);
-
- assertThat(message.type()).isEqualTo("UnsubscribeConfirmation");
- assertThat(message.messageId()).isEqualTo("12345678-1234-1234-1234-123456789012");
- assertThat(message.topicArn()).isEqualTo("arn:aws:sns:us-east-1:123456789012:MyTopic");
- assertThat(message.subject()).isEmpty();
- assertThat(message.message()).isEqualTo("You have been unsubscribed from the topic");
- assertThat(message.timestamp()).isEqualTo(Instant.parse("2023-01-01T12:00:00.000Z"));
- assertThat(message.signatureVersion()).isEqualTo("1");
- assertThat(message.signature()).isEqualTo("test-signature");
- assertThat(message.signingCertUrl()).isEqualTo("https://sns.us-east-1.amazonaws.com/cert.pem");
- assertThat(message.unsubscribeUrl()).isEmpty();
- assertThat(message.token()).hasValue("unsubscribe-token-12345");
- assertThat(message.messageAttributes()).isEmpty();
- }
-
- @Test
- void parseMessage_messageWithMessageAttributes_parsesSuccessfully() {
- String jsonWithAttributes = "{"
- + "\"Type\":\"Notification\","
- + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\","
- + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\","
- + "\"Message\":\"Test message content\","
- + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\","
- + "\"SignatureVersion\":\"1\","
- + "\"Signature\":\"test-signature\","
- + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\","
- + "\"MessageAttributes\":{"
- + "\"attr1\":\"value1\","
- + "\"attr2\":\"value2\""
- + "}"
- + "}";
-
- SnsMessage message = SnsMessageParser.parseMessage(jsonWithAttributes);
-
- assertThat(message.messageAttributes()).hasSize(2);
- assertThat(message.messageAttributes()).containsEntry("attr1", "value1");
- assertThat(message.messageAttributes()).containsEntry("attr2", "value2");
- }
-
- @Test
- void parseMessage_nullInput_throwsException() {
- assertThatThrownBy(() -> SnsMessageParser.parseMessage(null))
- .isInstanceOf(NullPointerException.class)
- .hasMessageContaining("messageJson must not be null");
- }
-
- @Test
- void parseMessage_emptyString_throwsException() {
- assertThatThrownBy(() -> SnsMessageParser.parseMessage(""))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("Message JSON cannot be empty or blank");
- }
-
- @Test
- void parseMessage_blankString_throwsException() {
- assertThatThrownBy(() -> SnsMessageParser.parseMessage(" "))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("Message JSON cannot be empty or blank");
- }
-
- @Test
- void parseMessage_tooLargeMessage_throwsException() {
- StringBuilder largeMessage = new StringBuilder();
- for (int i = 0; i < 300000; i++) { // Over 256KB
- largeMessage.append("a");
- }
-
- assertThatThrownBy(() -> SnsMessageParser.parseMessage(largeMessage.toString()))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("Message JSON is too large");
- }
-
- @Test
- void parseMessage_invalidJsonFormat_throwsException() {
- String invalidJson = "{ invalid json }";
-
- assertThatThrownBy(() -> SnsMessageParser.parseMessage(invalidJson))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("Failed to parse JSON message");
- }
-
- @Test
- void parseMessage_notJsonObject_throwsException() {
- String jsonArray = "[\"not\", \"an\", \"object\"]";
-
- assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonArray))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("Message JSON must start with '{'");
- }
-
- @Test
- void parseMessage_emptyJsonObject_throwsException() {
- assertThatThrownBy(() -> SnsMessageParser.parseMessage("{}"))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("Message cannot be empty");
- }
-
- @Test
- void parseMessage_missingType_throwsException() {
- String jsonWithoutType = "{"
- + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\","
- + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\","
- + "\"Message\":\"Test message content\","
- + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\","
- + "\"SignatureVersion\":\"1\","
- + "\"Signature\":\"test-signature\","
- + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\""
- + "}";
-
- assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithoutType))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("Required field 'Type' is missing");
- }
-
- @Test
- void parseMessage_missingMessageId_throwsException() {
- String jsonWithoutMessageId = "{"
- + "\"Type\":\"Notification\","
- + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\","
- + "\"Message\":\"Test message content\","
- + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\","
- + "\"SignatureVersion\":\"1\","
- + "\"Signature\":\"test-signature\","
- + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\""
- + "}";
-
- assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithoutMessageId))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("Missing required fields")
- .hasMessageContaining("MessageId");
- }
-
- @Test
- void parseMessage_missingTopicArn_throwsException() {
- String jsonWithoutTopicArn = "{"
- + "\"Type\":\"Notification\","
- + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\","
- + "\"Message\":\"Test message content\","
- + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\","
- + "\"SignatureVersion\":\"1\","
- + "\"Signature\":\"test-signature\","
- + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\""
- + "}";
-
- assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithoutTopicArn))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("Missing required fields")
- .hasMessageContaining("TopicArn");
- }
-
- @Test
- void parseMessage_missingMessage_throwsException() {
- String jsonWithoutMessage = "{"
- + "\"Type\":\"Notification\","
- + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\","
- + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\","
- + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\","
- + "\"SignatureVersion\":\"1\","
- + "\"Signature\":\"test-signature\","
- + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\""
- + "}";
-
- assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithoutMessage))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("Missing required fields")
- .hasMessageContaining("Message");
- }
-
- @Test
- void parseMessage_missingTimestamp_throwsException() {
- String jsonWithoutTimestamp = "{"
- + "\"Type\":\"Notification\","
- + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\","
- + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\","
- + "\"Message\":\"Test message content\","
- + "\"SignatureVersion\":\"1\","
- + "\"Signature\":\"test-signature\","
- + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\""
- + "}";
-
- assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithoutTimestamp))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("Missing required fields")
- .hasMessageContaining("Timestamp");
- }
-
- @Test
- void parseMessage_missingSignatureVersion_throwsException() {
- String jsonWithoutSignatureVersion = "{"
- + "\"Type\":\"Notification\","
- + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\","
- + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\","
- + "\"Message\":\"Test message content\","
- + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\","
- + "\"Signature\":\"test-signature\","
- + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\""
- + "}";
-
- assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithoutSignatureVersion))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("Missing required fields")
- .hasMessageContaining("SignatureVersion");
- }
-
- @Test
- void parseMessage_missingSignature_throwsException() {
- String jsonWithoutSignature = "{"
- + "\"Type\":\"Notification\","
- + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\","
- + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\","
- + "\"Message\":\"Test message content\","
- + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\","
- + "\"SignatureVersion\":\"1\","
- + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\""
- + "}";
-
- assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithoutSignature))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("Missing required fields")
- .hasMessageContaining("Signature");
- }
-
- @Test
- void parseMessage_missingSigningCertURL_throwsException() {
- String jsonWithoutSigningCertURL = "{"
- + "\"Type\":\"Notification\","
- + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\","
- + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\","
- + "\"Message\":\"Test message content\","
- + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\","
- + "\"SignatureVersion\":\"1\","
- + "\"Signature\":\"test-signature\""
- + "}";
-
- assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithoutSigningCertURL))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("Missing required fields")
- .hasMessageContaining("SigningCertURL");
- }
-
- @Test
- void parseMessage_missingTokenForSubscriptionConfirmation_throwsException() {
- String jsonWithoutToken = "{"
- + "\"Type\":\"SubscriptionConfirmation\","
- + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\","
- + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\","
- + "\"Message\":\"You have chosen to subscribe to the topic\","
- + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\","
- + "\"SignatureVersion\":\"2\","
- + "\"Signature\":\"test-signature\","
- + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\""
- + "}";
-
- assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithoutToken))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("Missing required fields")
- .hasMessageContaining("Token");
- }
-
- @Test
- void parseMessage_unsupportedMessageType_throwsException() {
- String jsonWithUnsupportedType = "{"
- + "\"Type\":\"UnsupportedType\","
- + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\","
- + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\","
- + "\"Message\":\"Test message content\","
- + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\","
- + "\"SignatureVersion\":\"1\","
- + "\"Signature\":\"test-signature\","
- + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\""
- + "}";
-
- assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithUnsupportedType))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("Unsupported message type: UnsupportedType")
- .hasMessageContaining("Supported types are: Notification, SubscriptionConfirmation, UnsubscribeConfirmation");
- }
-
- @Test
- void parseMessage_unexpectedFields_throwsException() {
- String jsonWithUnexpectedField = "{"
- + "\"Type\":\"Notification\","
- + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\","
- + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\","
- + "\"Message\":\"Test message content\","
- + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\","
- + "\"SignatureVersion\":\"1\","
- + "\"Signature\":\"test-signature\","
- + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\","
- + "\"UnexpectedField\":\"unexpected-value\""
- + "}";
-
- assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithUnexpectedField))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("Message contains unexpected fields")
- .hasMessageContaining("UnexpectedField");
- }
-
- @Test
- void parseMessage_nullFieldValue_throwsException() {
- String jsonWithNullField = "{"
- + "\"Type\":\"Notification\","
- + "\"MessageId\":null,"
- + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\","
- + "\"Message\":\"Test message content\","
- + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\","
- + "\"SignatureVersion\":\"1\","
- + "\"Signature\":\"test-signature\","
- + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\""
- + "}";
-
- assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithNullField))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("Missing required fields")
- .hasMessageContaining("MessageId");
- }
-
- @Test
- void parseMessage_emptyFieldValue_throwsException() {
- String jsonWithEmptyField = "{"
- + "\"Type\":\"Notification\","
- + "\"MessageId\":\"\","
- + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\","
- + "\"Message\":\"Test message content\","
- + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\","
- + "\"SignatureVersion\":\"1\","
- + "\"Signature\":\"test-signature\","
- + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\""
- + "}";
-
- assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithEmptyField))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("Required field 'MessageId' cannot be empty or blank");
- }
-
- @Test
- void parseMessage_nonStringFieldValue_throwsException() {
- String jsonWithNonStringField = "{"
- + "\"Type\":\"Notification\","
- + "\"MessageId\":12345,"
- + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\","
- + "\"Message\":\"Test message content\","
- + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\","
- + "\"SignatureVersion\":\"1\","
- + "\"Signature\":\"test-signature\","
- + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\""
- + "}";
-
- assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithNonStringField))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("Field 'MessageId' must be a string but found number");
- }
-
- @Test
- void parseMessage_invalidTimestampFormat_throwsException() {
- String jsonWithInvalidTimestamp = "{"
- + "\"Type\":\"Notification\","
- + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\","
- + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\","
- + "\"Message\":\"Test message content\","
- + "\"Timestamp\":\"invalid-timestamp\","
- + "\"SignatureVersion\":\"1\","
- + "\"Signature\":\"test-signature\","
- + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\""
- + "}";
-
- assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithInvalidTimestamp))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("Invalid timestamp format: invalid-timestamp");
- }
-
- @Test
- void parseMessage_invalidTopicArn_throwsException() {
- String jsonWithInvalidTopicArn = "{"
- + "\"Type\":\"Notification\","
- + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\","
- + "\"TopicArn\":\"invalid-arn\","
- + "\"Message\":\"Test message content\","
- + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\","
- + "\"SignatureVersion\":\"1\","
- + "\"Signature\":\"test-signature\","
- + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\""
- + "}";
-
- assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithInvalidTopicArn))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("TopicArn must be a valid ARN starting with 'arn:'");
- }
-
- @Test
- void parseMessage_nonSnsTopicArn_throwsException() {
- String jsonWithNonSnsArn = "{"
- + "\"Type\":\"Notification\","
- + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\","
- + "\"TopicArn\":\"arn:aws:s3:::my-bucket\","
- + "\"Message\":\"Test message content\","
- + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\","
- + "\"SignatureVersion\":\"1\","
- + "\"Signature\":\"test-signature\","
- + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\""
- + "}";
-
- assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithNonSnsArn))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("TopicArn must be an SNS topic ARN containing ':sns:'");
- }
-
- @Test
- void parseMessage_invalidSignatureVersion_throwsException() {
- String jsonWithInvalidSignatureVersion = "{"
- + "\"Type\":\"Notification\","
- + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\","
- + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\","
- + "\"Message\":\"Test message content\","
- + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\","
- + "\"SignatureVersion\":\"3\","
- + "\"Signature\":\"test-signature\","
- + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\""
- + "}";
-
- assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithInvalidSignatureVersion))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("SignatureVersion must be '1' or '2'. Received: '3'");
- }
-
- @Test
- void parseMessage_nonHttpsSigningCertURL_throwsException() {
- String jsonWithHttpCertUrl = "{"
- + "\"Type\":\"Notification\","
- + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\","
- + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\","
- + "\"Message\":\"Test message content\","
- + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\","
- + "\"SignatureVersion\":\"1\","
- + "\"Signature\":\"test-signature\","
- + "\"SigningCertURL\":\"http://sns.us-east-1.amazonaws.com/cert.pem\""
- + "}";
-
- assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithHttpCertUrl))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("SigningCertURL must use HTTPS protocol for security");
- }
-
- @Test
- void parseMessage_nonHttpsUnsubscribeURL_throwsException() {
- String jsonWithHttpUnsubscribeUrl = "{"
- + "\"Type\":\"Notification\","
- + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\","
- + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\","
- + "\"Message\":\"Test message content\","
- + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\","
- + "\"SignatureVersion\":\"1\","
- + "\"Signature\":\"test-signature\","
- + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\","
- + "\"UnsubscribeURL\":\"http://sns.us-east-1.amazonaws.com/unsubscribe\""
- + "}";
-
- assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithHttpUnsubscribeUrl))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("UnsubscribeURL must use HTTPS protocol for security");
- }
-
- @Test
- void parseMessage_tooLongMessageId_throwsException() {
- StringBuilder longMessageId = new StringBuilder();
- for (int i = 0; i < 101; i++) { // Over 100 characters
- longMessageId.append("a");
- }
-
- String jsonWithLongMessageId = "{"
- + "\"Type\":\"Notification\","
- + "\"MessageId\":\"" + longMessageId.toString() + "\","
- + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\","
- + "\"Message\":\"Test message content\","
- + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\","
- + "\"SignatureVersion\":\"1\","
- + "\"Signature\":\"test-signature\","
- + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\""
- + "}";
-
- assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithLongMessageId))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("MessageId is too long");
- }
-
- @Test
- void parseMessage_invalidMessageAttributesType_throwsException() {
- String jsonWithInvalidMessageAttributes = "{"
- + "\"Type\":\"Notification\","
- + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\","
- + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\","
- + "\"Message\":\"Test message content\","
- + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\","
- + "\"SignatureVersion\":\"1\","
- + "\"Signature\":\"test-signature\","
- + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\","
- + "\"MessageAttributes\":\"not-an-object\""
- + "}";
-
- assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithInvalidMessageAttributes))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("MessageAttributes must be a JSON object");
- }
-
- @Test
- void parseMessage_invalidMessageAttributeValueType_throwsException() {
- String jsonWithInvalidAttributeValue = "{"
- + "\"Type\":\"Notification\","
- + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\","
- + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\","
- + "\"Message\":\"Test message content\","
- + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\","
- + "\"SignatureVersion\":\"1\","
- + "\"Signature\":\"test-signature\","
- + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\","
- + "\"MessageAttributes\":{"
- + "\"attr1\":123"
- + "}"
- + "}";
-
- assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithInvalidAttributeValue))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("MessageAttribute value for key 'attr1' must be a string");
- }
-
- @Test
- void parseMessage_nullMessageAttributeValue_skipsAttribute() {
- String jsonWithNullAttributeValue = "{"
- + "\"Type\":\"Notification\","
- + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\","
- + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\","
- + "\"Message\":\"Test message content\","
- + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\","
- + "\"SignatureVersion\":\"1\","
- + "\"Signature\":\"test-signature\","
- + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\","
- + "\"MessageAttributes\":{"
- + "\"attr1\":\"value1\","
- + "\"attr2\":null,"
- + "\"attr3\":\"value3\""
- + "}"
- + "}";
-
- SnsMessage message = SnsMessageParser.parseMessage(jsonWithNullAttributeValue);
-
- assertThat(message.messageAttributes()).hasSize(2);
- assertThat(message.messageAttributes()).containsEntry("attr1", "value1");
- assertThat(message.messageAttributes()).containsEntry("attr3", "value3");
- assertThat(message.messageAttributes()).doesNotContainKey("attr2");
- }
-
- @Test
- void parseMessage_unbalancedBraces_throwsException() {
- String jsonWithUnbalancedBraces = "{"
- + "\"Type\":\"Notification\","
- + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\"";
- // Missing closing brace
-
- assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithUnbalancedBraces))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("Message JSON must end with '}'");
- }
-
- @Test
- void parseMessage_doesNotStartWithBrace_throwsException() {
- String invalidJson = "invalid{\"Type\":\"Notification\"}";
-
- assertThatThrownBy(() -> SnsMessageParser.parseMessage(invalidJson))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("Message JSON must start with '{'");
- }
-
- @Test
- void parseMessage_doesNotEndWithBrace_throwsException() {
- String invalidJson = "{\"Type\":\"Notification\"}invalid";
-
- assertThatThrownBy(() -> SnsMessageParser.parseMessage(invalidJson))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("Message JSON must end with '}'");
- }
-}
\ No newline at end of file
diff --git a/services/sns/src/test/java/software/amazon/awssdk/services/sns/messagemanager/SnsMessageManagerIntegrationTest.java b/services/sns/src/test/java/software/amazon/awssdk/services/sns/messagemanager/SnsMessageManagerIntegrationTest.java
deleted file mode 100644
index a563e2d58832..000000000000
--- a/services/sns/src/test/java/software/amazon/awssdk/services/sns/messagemanager/SnsMessageManagerIntegrationTest.java
+++ /dev/null
@@ -1,393 +0,0 @@
-/*
- * 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.services.sns.messagemanager;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-
-import java.io.ByteArrayInputStream;
-import java.nio.charset.StandardCharsets;
-import java.time.Duration;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-/**
- * Integration tests for SnsMessageManager that verify the complete workflow
- * from public API through all internal components.
- */
-class SnsMessageManagerIntegrationTest {
-
- private SnsMessageManager messageManager;
-
- @BeforeEach
- void setUp() {
- messageManager = SnsMessageManager.builder().build();
- }
-
- @AfterEach
- void tearDown() {
- if (messageManager != null) {
- messageManager.close();
- }
- }
-
- @Test
- void builder_withDefaultConfiguration_createsManagerSuccessfully() {
- try (SnsMessageManager manager = SnsMessageManager.builder().build()) {
- assertThat(manager).isNotNull();
- }
- }
-
- @Test
- void builder_withCustomConfiguration_createsManagerSuccessfully() {
- MessageManagerConfiguration config = MessageManagerConfiguration.builder()
- .certificateCacheTimeout(Duration.ofMinutes(10))
- .build();
-
- try (SnsMessageManager manager = SnsMessageManager.builder()
- .configuration(config)
- .build()) {
- assertThat(manager).isNotNull();
- }
- }
-
- @Test
- void builder_withConsumerConfiguration_createsManagerSuccessfully() {
- try (SnsMessageManager manager = SnsMessageManager.builder()
- .configuration(config -> config.certificateCacheTimeout(Duration.ofMinutes(15)))
- .build()) {
- assertThat(manager).isNotNull();
- }
- }
-
- @Test
- void parseMessage_withNullString_throwsException() {
- assertThatThrownBy(() -> messageManager.parseMessage((String) null))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("Message content cannot be null");
- }
-
- @Test
- void parseMessage_withNullInputStream_throwsException() {
- assertThatThrownBy(() -> messageManager.parseMessage((ByteArrayInputStream) null))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("Message InputStream cannot be null");
- }
-
- @Test
- void parseMessage_withEmptyString_throwsException() {
- assertThatThrownBy(() -> messageManager.parseMessage(""))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("Message content cannot be empty");
- }
-
- @Test
- void parseMessage_withWhitespaceOnlyString_throwsException() {
- assertThatThrownBy(() -> messageManager.parseMessage(" \n\t "))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("Message content cannot be empty");
- }
-
- @Test
- void parseMessage_withInvalidJson_throwsParsingException() {
- String invalidJson = "{ invalid json }";
-
- assertThatThrownBy(() -> messageManager.parseMessage(invalidJson))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("Failed to parse JSON message");
- }
-
- @Test
- void parseMessage_withNonJsonString_throwsParsingException() {
- String nonJson = "This is not JSON";
-
- assertThatThrownBy(() -> messageManager.parseMessage(nonJson))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("Message content does not appear to be valid JSON");
- }
-
- @Test
- void parseMessage_withValidJsonButMissingRequiredFields_throwsParsingException() {
- String incompleteMessage = "{\"Type\": \"Notification\"}";
-
- assertThatThrownBy(() -> messageManager.parseMessage(incompleteMessage))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("Missing required fields");
- }
-
- @Test
- void parseMessage_withUnsupportedMessageType_throwsParsingException() {
- String messageWithInvalidType = "{"
- + "\"Type\": \"InvalidType\","
- + "\"MessageId\": \"test-id\","
- + "\"TopicArn\": \"arn:aws:sns:us-east-1:123456789012:test-topic\","
- + "\"Message\": \"test message\","
- + "\"Timestamp\": \"2023-01-01T00:00:00.000Z\","
- + "\"SignatureVersion\": \"1\","
- + "\"Signature\": \"test-signature\","
- + "\"SigningCertURL\": \"https://sns.us-east-1.amazonaws.com/test.pem\""
- + "}";
-
- assertThatThrownBy(() -> messageManager.parseMessage(messageWithInvalidType))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("Unsupported message type: InvalidType");
- }
-
- @Test
- void parseMessage_withInvalidCertificateUrl_throwsParsingException() {
- String messageWithInvalidCertUrl = "{"
- + "\"Type\": \"Notification\","
- + "\"MessageId\": \"test-id\","
- + "\"TopicArn\": \"arn:aws:sns:us-east-1:123456789012:test-topic\","
- + "\"Message\": \"test message\","
- + "\"Timestamp\": \"2023-01-01T00:00:00.000Z\","
- + "\"SignatureVersion\": \"1\","
- + "\"Signature\": \"test-signature\","
- + "\"SigningCertURL\": \"http://malicious-site.com/fake-cert.pem\""
- + "}";
-
- assertThatThrownBy(() -> messageManager.parseMessage(messageWithInvalidCertUrl))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("SigningCertURL must use HTTPS protocol for security");
- }
-
- @Test
- void parseMessage_withHttpCertificateUrl_throwsParsingException() {
- String messageWithHttpCertUrl = "{"
- + "\"Type\": \"Notification\","
- + "\"MessageId\": \"test-id\","
- + "\"TopicArn\": \"arn:aws:sns:us-east-1:123456789012:test-topic\","
- + "\"Message\": \"test message\","
- + "\"Timestamp\": \"2023-01-01T00:00:00.000Z\","
- + "\"SignatureVersion\": \"1\","
- + "\"Signature\": \"test-signature\","
- + "\"SigningCertURL\": \"http://sns.us-east-1.amazonaws.com/test.pem\""
- + "}";
-
- assertThatThrownBy(() -> messageManager.parseMessage(messageWithHttpCertUrl))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("SigningCertURL must use HTTPS protocol for security");
- }
-
- @Test
- void parseMessage_withValidUrlButNetworkFailure_throwsCertificateException() {
- // This test uses a valid HTTPS SNS URL that will pass parsing but fail during certificate retrieval
- String messageWithValidButUnreachableUrl = "{"
- + "\"Type\": \"Notification\","
- + "\"MessageId\": \"test-id\","
- + "\"TopicArn\": \"arn:aws:sns:us-east-1:123456789012:test-topic\","
- + "\"Message\": \"test message\","
- + "\"Timestamp\": \"2023-01-01T00:00:00.000Z\","
- + "\"SignatureVersion\": \"1\","
- + "\"Signature\": \"test-signature\","
- + "\"SigningCertURL\": \"https://sns.us-east-1.amazonaws.com/nonexistent-cert.pem\""
- + "}";
-
- // This should pass parsing but fail during certificate retrieval
- assertThatThrownBy(() -> messageManager.parseMessage(messageWithValidButUnreachableUrl))
- .isInstanceOf(SnsCertificateException.class)
- .hasMessageContaining("Failed to retrieve certificate");
- }
-
- @Test
- void parseMessage_withInputStream_handlesParsingCorrectly() {
- String invalidJson = "{ invalid json }";
- ByteArrayInputStream inputStream = new ByteArrayInputStream(invalidJson.getBytes(StandardCharsets.UTF_8));
-
- assertThatThrownBy(() -> messageManager.parseMessage(inputStream))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("Failed to parse JSON message");
- }
-
- @Test
- void parseMessage_withEmptyInputStream_throwsException() {
- ByteArrayInputStream emptyStream = new ByteArrayInputStream(new byte[0]);
-
- assertThatThrownBy(() -> messageManager.parseMessage(emptyStream))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("InputStream is empty");
- }
-
- @Test
- void parseMessage_withLargeMessage_throwsException() {
- // Create a message larger than 256KB
- StringBuilder largeMessage = new StringBuilder("{\"Type\": \"Notification\",");
- largeMessage.append("\"Message\": \"");
- for (int i = 0; i < 300 * 1024; i++) { // 300KB of 'a' characters
- largeMessage.append("a");
- }
- largeMessage.append("\"}");
-
- assertThatThrownBy(() -> messageManager.parseMessage(largeMessage.toString()))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("Message content is too large");
- }
-
- @Test
- void parseMessage_withInvalidTopicArn_throwsParsingException() {
- String messageWithInvalidArn = "{"
- + "\"Type\": \"Notification\","
- + "\"MessageId\": \"test-id\","
- + "\"TopicArn\": \"invalid-arn\","
- + "\"Message\": \"test message\","
- + "\"Timestamp\": \"2023-01-01T00:00:00.000Z\","
- + "\"SignatureVersion\": \"1\","
- + "\"Signature\": \"test-signature\","
- + "\"SigningCertURL\": \"https://sns.us-east-1.amazonaws.com/test.pem\""
- + "}";
-
- assertThatThrownBy(() -> messageManager.parseMessage(messageWithInvalidArn))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("TopicArn must be a valid ARN starting with 'arn:'");
- }
-
- @Test
- void parseMessage_withInvalidSignatureVersion_throwsParsingException() {
- String messageWithInvalidSigVersion = "{"
- + "\"Type\": \"Notification\","
- + "\"MessageId\": \"test-id\","
- + "\"TopicArn\": \"arn:aws:sns:us-east-1:123456789012:test-topic\","
- + "\"Message\": \"test message\","
- + "\"Timestamp\": \"2023-01-01T00:00:00.000Z\","
- + "\"SignatureVersion\": \"3\","
- + "\"Signature\": \"test-signature\","
- + "\"SigningCertURL\": \"https://sns.us-east-1.amazonaws.com/test.pem\""
- + "}";
-
- assertThatThrownBy(() -> messageManager.parseMessage(messageWithInvalidSigVersion))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("SignatureVersion must be '1' or '2'");
- }
-
- @Test
- void parseMessage_withInvalidTimestamp_throwsParsingException() {
- String messageWithInvalidTimestamp = "{"
- + "\"Type\": \"Notification\","
- + "\"MessageId\": \"test-id\","
- + "\"TopicArn\": \"arn:aws:sns:us-east-1:123456789012:test-topic\","
- + "\"Message\": \"test message\","
- + "\"Timestamp\": \"invalid-timestamp\","
- + "\"SignatureVersion\": \"1\","
- + "\"Signature\": \"test-signature\","
- + "\"SigningCertURL\": \"https://sns.us-east-1.amazonaws.com/test.pem\""
- + "}";
-
- assertThatThrownBy(() -> messageManager.parseMessage(messageWithInvalidTimestamp))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("Invalid timestamp format");
- }
-
- @Test
- void parseMessage_withUnexpectedFields_throwsParsingException() {
- String messageWithUnexpectedField = "{"
- + "\"Type\": \"Notification\","
- + "\"MessageId\": \"test-id\","
- + "\"TopicArn\": \"arn:aws:sns:us-east-1:123456789012:test-topic\","
- + "\"Message\": \"test message\","
- + "\"Timestamp\": \"2023-01-01T00:00:00.000Z\","
- + "\"SignatureVersion\": \"1\","
- + "\"Signature\": \"test-signature\","
- + "\"SigningCertURL\": \"https://sns.us-east-1.amazonaws.com/test.pem\","
- + "\"UnexpectedField\": \"should not be here\""
- + "}";
-
- assertThatThrownBy(() -> messageManager.parseMessage(messageWithUnexpectedField))
- .isInstanceOf(SnsMessageParsingException.class)
- .hasMessageContaining("Message contains unexpected fields");
- }
-
- @Test
- void close_withDefaultHttpClient_closesSuccessfully() {
- SnsMessageManager manager = SnsMessageManager.builder().build();
-
- // Should not throw any exception
- manager.close();
- }
-
- @Test
- void close_multipleCallsToClose_handlesGracefully() {
- SnsMessageManager manager = SnsMessageManager.builder().build();
-
- // Multiple calls to close should not throw exceptions
- manager.close();
- manager.close();
- manager.close();
- }
-
- @Test
- void messageManagerConfiguration_builderPattern_worksCorrectly() {
- Duration customTimeout = Duration.ofHours(2);
-
- MessageManagerConfiguration config = MessageManagerConfiguration.builder()
- .certificateCacheTimeout(customTimeout)
- .build();
-
- assertThat(config.certificateCacheTimeout()).isEqualTo(customTimeout);
- assertThat(config.httpClient()).isNull(); // Default should be null
- }
-
- @Test
- void messageManagerConfiguration_toBuilder_preservesValues() {
- Duration originalTimeout = Duration.ofMinutes(30);
-
- MessageManagerConfiguration original = MessageManagerConfiguration.builder()
- .certificateCacheTimeout(originalTimeout)
- .build();
-
- MessageManagerConfiguration copy = original.toBuilder()
- .certificateCacheTimeout(Duration.ofHours(1))
- .build();
-
- assertThat(original.certificateCacheTimeout()).isEqualTo(originalTimeout);
- assertThat(copy.certificateCacheTimeout()).isEqualTo(Duration.ofHours(1));
- }
-
- @Test
- void messageManagerConfiguration_equalsAndHashCode_workCorrectly() {
- Duration timeout = Duration.ofMinutes(10);
-
- MessageManagerConfiguration config1 = MessageManagerConfiguration.builder()
- .certificateCacheTimeout(timeout)
- .build();
-
- MessageManagerConfiguration config2 = MessageManagerConfiguration.builder()
- .certificateCacheTimeout(timeout)
- .build();
-
- MessageManagerConfiguration config3 = MessageManagerConfiguration.builder()
- .certificateCacheTimeout(Duration.ofMinutes(20))
- .build();
-
- assertThat(config1).isEqualTo(config2);
- assertThat(config1).isNotEqualTo(config3);
- assertThat(config1.hashCode()).isEqualTo(config2.hashCode());
- assertThat(config1.hashCode()).isNotEqualTo(config3.hashCode());
- }
-
- @Test
- void messageManagerConfiguration_toString_containsExpectedFields() {
- Duration timeout = Duration.ofMinutes(5);
-
- MessageManagerConfiguration config = MessageManagerConfiguration.builder()
- .certificateCacheTimeout(timeout)
- .build();
-
- String toString = config.toString();
- assertThat(toString).contains("MessageManagerConfiguration");
- assertThat(toString).contains("certificateCacheTimeout");
- }
-}
\ No newline at end of file
- *
- *
- * @param certificateUrl The URL of the certificate to retrieve.
- * @return The certificate bytes.
- * @throws SnsCertificateException If certificate retrieval or validation fails.
- * @throws NullPointerException If certificateUrl is null.
- */
- public byte[] retrieveCertificate(String certificateUrl) {
- Validate.paramNotNull(certificateUrl, "certificateUrl");
-
- // Check cache first
- CachedCertificate cached = certificateCache.get(certificateUrl);
- if (cached != null && !cached.isExpired()) {
- return cached.getCertificateBytes();
- }
-
- // Validate certificate URL security
- validateCertificateUrl(certificateUrl);
-
- // Retrieve certificate from AWS
- byte[] certificateBytes = fetchCertificateFromUrl(certificateUrl);
-
- // Cache the certificate
- certificateCache.put(certificateUrl, new CachedCertificate(certificateBytes, certificateCacheTimeout));
-
- return certificateBytes;
- }
-
- /**
- * Validates that the certificate URL is from a trusted SNS domain and uses HTTPS.
- *
- * @param certificateUrl The certificate URL to validate.
- * @throws SnsCertificateException If the URL is not trusted or secure.
- */
- private void validateCertificateUrl(String certificateUrl) {
- if (StringUtils.isBlank(certificateUrl)) {
- throw SnsCertificateException.builder()
- .message("Certificate URL cannot be null or empty")
- .build();
- }
-
- URI uri;
- try {
- uri = new URI(certificateUrl);
- } catch (URISyntaxException e) {
- throw SnsCertificateException.builder()
- .message("Invalid certificate URL format: " + certificateUrl)
- .cause(e)
- .build();
- }
-
- // Ensure HTTPS only
- if (!HTTPS_SCHEME.equalsIgnoreCase(uri.getScheme())) {
- throw SnsCertificateException.builder()
- .message("Certificate URL must use HTTPS. Provided URL: " + certificateUrl)
- .build();
- }
-
- // Validate against trusted SNS domain patterns
- String host = uri.getHost();
- if (host == null || !isTrustedSnsDomain(host)) {
- throw SnsCertificateException.builder()
- .message("Certificate URL is not from a trusted SNS domain. Host: " + host +
- ". Expected format: sns.
- *
- *
- *
- *
- * @param host The host to check.
- * @return true if the host matches a trusted SNS domain pattern, false otherwise.
- */
- private boolean isTrustedSnsDomain(String host) {
- if (host == null) {
- return false;
- }
-
- // Convert to lowercase for case-insensitive matching
- String normalizedHost = host.toLowerCase();
-
- // Check against all trusted SNS domain patterns
- for (Pattern pattern : TRUSTED_SNS_DOMAIN_PATTERNS) {
- if (pattern.matcher(normalizedHost).matches()) {
- return true;
- }
- }
-
- return false;
- }
-
- /**
- * Fetches the certificate from the specified URL.
- *
- * @param certificateUrl The URL to fetch the certificate from.
- * @return The certificate bytes.
- * @throws SnsCertificateException If certificate retrieval fails.
- */
- private byte[] fetchCertificateFromUrl(String certificateUrl) {
- SdkHttpRequest httpRequest = SdkHttpRequest.builder()
- .method(SdkHttpMethod.GET)
- .uri(URI.create(certificateUrl))
- .build();
-
- HttpExecuteRequest executeRequest = HttpExecuteRequest.builder()
- .request(httpRequest)
- .build();
-
- try {
- HttpExecuteResponse response = httpClient.prepareRequest(executeRequest).call();
-
- if (!response.httpResponse().isSuccessful()) {
- throw SnsCertificateException.builder()
- .message("Failed to retrieve certificate from URL: " + certificateUrl +
- ". HTTP status: " + response.httpResponse().statusCode())
- .build();
- }
-
- return readCertificateBytes(response);
-
- } catch (IOException e) {
- throw SnsCertificateException.builder()
- .message("IO error while retrieving certificate from URL: " + certificateUrl)
- .cause(e)
- .build();
- } catch (Exception e) {
- throw SnsCertificateException.builder()
- .message("Unexpected error while retrieving certificate from URL: " + certificateUrl)
- .cause(e)
- .build();
- }
- }
-
- /**
- * Reads certificate bytes from the HTTP response with comprehensive validation.
- *
- * @param response The HTTP response containing the certificate.
- * @return The certificate bytes.
- * @throws IOException If reading fails.
- * @throws SnsCertificateException If certificate validation fails.
- */
- private byte[] readCertificateBytes(HttpExecuteResponse response) throws IOException {
- try (InputStream inputStream = response.responseBody().orElseThrow(
- () -> SnsCertificateException.builder()
- .message("Certificate response body is empty")
- .build())) {
-
- ByteArrayOutputStream buffer = new ByteArrayOutputStream();
- byte[] chunk = new byte[1024];
- int totalBytesRead = 0;
- int bytesRead;
-
- while ((bytesRead = inputStream.read(chunk)) != -1) {
- totalBytesRead += bytesRead;
-
- // Protect against oversized certificates
- if (totalBytesRead > MAX_CERTIFICATE_SIZE) {
- throw SnsCertificateException.builder()
- .message("Certificate size exceeds maximum allowed size of " + MAX_CERTIFICATE_SIZE + " bytes")
- .build();
- }
-
- buffer.write(chunk, 0, bytesRead);
- }
-
- byte[] certificateBytes = buffer.toByteArray();
-
- if (certificateBytes.length == 0) {
- throw SnsCertificateException.builder()
- .message("Retrieved certificate is empty")
- .build();
- }
-
- // Perform additional security validation on certificate content
- validateCertificateContent(certificateBytes);
-
- return certificateBytes;
- }
- }
-
- /**
- * Validates the certificate content for security compliance.
- *
- *
- *
- *
- *
- *
- *
- *
- *
- *
- *
- *
- *
- *
- *
- * @param message The SNS message to validate. Must contain all required signature fields
- * including signature, signatureVersion, and message content.
- * @param certificateBytes The X.509 certificate bytes in PEM or DER format used for
- * signature verification. Must be a valid certificate from Amazon SNS.
- * @throws SnsSignatureValidationException If signature verification fails, indicating the
- * message may have been tampered with or is not from Amazon SNS
- * @throws SnsCertificateException If certificate processing, parsing, or validation fails
- * @throws NullPointerException If message or certificateBytes is null
- */
- public static void validateSignature(SnsMessage message, byte[] certificateBytes) {
- Validate.paramNotNull(message, "message");
- Validate.paramNotNull(certificateBytes, "certificateBytes");
-
- X509Certificate certificate = parseCertificate(certificateBytes);
- validateCertificate(certificate);
-
- String signatureAlgorithm = getSignatureAlgorithm(message.signatureVersion());
- String canonicalMessage = buildCanonicalMessage(message);
-
- verifySignature(message.signature(), canonicalMessage, certificate.getPublicKey(), signatureAlgorithm);
- }
-
- private static X509Certificate parseCertificate(byte[] certificateBytes) {
- try {
- CertificateFactory certificateFactory = CertificateFactory.getInstance(CERTIFICATE_TYPE);
- Certificate certificate = certificateFactory.generateCertificate(
- new ByteArrayInputStream(certificateBytes));
-
- if (!(certificate instanceof X509Certificate)) {
- throw SnsCertificateException.builder()
- .message("Certificate is not an X.509 certificate")
- .build();
- }
-
- return (X509Certificate) certificate;
- } catch (CertificateException e) {
- throw SnsCertificateException.builder()
- .message("Failed to parse certificate: " + e.getMessage())
- .cause(e)
- .build();
- }
- }
-
- private static void validateCertificate(X509Certificate certificate) {
- try {
- // Check certificate validity period
- certificate.checkValidity();
-
- // Verify certificate is issued by Amazon SNS with comprehensive chain validation
- validateCertificateChainOfTrust(certificate);
-
- // Additional security checks
- validateCertificateKeyUsage(certificate);
-
- } catch (CertificateException e) {
- throw SnsCertificateException.builder()
- .message("Certificate validation failed: " + e.getMessage())
- .cause(e)
- .build();
- }
- }
-
- /**
- * Validates the certificate chain of trust to ensure it's issued by Amazon SNS.
- *
- * {@code
- * MessageManagerConfiguration config = MessageManagerConfiguration.builder()
- * .certificateCacheTimeout(Duration.ofHours(1))
- * .build();
- *
- * SnsMessageManager manager = SnsMessageManager.builder()
- * .configuration(config)
- * .build();
- * }
- *
- */
-@SdkPublicApi
-@Immutable
-@ThreadSafe
-public final class MessageManagerConfiguration
- implements ToCopyableBuilder
- *
- *
- *
- *
- * {@code
- * SnsMessageManager messageManager = SnsMessageManager.builder().build();
- * SnsMessage message = messageManager.parseMessage(jsonMessageBody);
- *
- * // Access message properties
- * String messageType = message.type();
- * String content = message.message();
- * String topicArn = message.topicArn();
- *
- * // Handle optional fields
- * message.subject().ifPresent(subject ->
- * System.out.println("Subject: " + subject));
- * }
- *
- * @see SnsMessageManager
- */
-@SdkPublicApi
-public final class SnsMessage {
-
- private final String type;
- private final String messageId;
- private final String topicArn;
- private final String subject;
- private final String message;
- private final Instant timestamp;
- private final String signatureVersion;
- private final String signature;
- private final String signingCertUrl;
- private final String unsubscribeUrl;
- private final String token;
- private final Map
- *
- *
- * @return The message type (never null).
- */
- public String type() {
- return type;
- }
-
- /**
- * Returns the unique message identifier.
- *
- * @return The message ID (never null).
- */
- public String messageId() {
- return messageId;
- }
-
- /**
- * Returns the Amazon Resource Name (ARN) of the topic from which the message was published.
- *
- * @return The topic ARN (never null).
- */
- public String topicArn() {
- return topicArn;
- }
-
- /**
- * Returns the subject of the message, if provided.
- *
- *
- *
- *
- * @return The signature version (never null).
- */
- public String signatureVersion() {
- return signatureVersion;
- }
-
- /**
- * Returns the cryptographic signature of the message.
- *
- * @return The message signature (never null).
- */
- public String signature() {
- return signature;
- }
-
- /**
- * Returns the URL of the certificate used to sign the message.
- *
- * {@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 (SnsMessageValidationException e) {
- * // Handle validation failure
- * logger.error("SNS message validation failed: {}", e.getMessage());
- * }
- * }
- *
- */
-@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 an InputStream.
- *
- *
- *
- *
- *
- *
- *
- *
- *
- * @see CertificateRetriever
- */
-class CertificateRetrieverTest {
-
- /** Valid certificate URL for US East 1 region used in tests. */
- private static final String VALID_CERT_URL_US_EAST_1 = "https://sns.us-east-1.amazonaws.com/cert.pem";
-
- /** Valid certificate URL for EU West 1 region used in tests. */
- private static final String VALID_CERT_URL_EU_WEST_1 = "https://sns.eu-west-1.amazonaws.com/cert.pem";
-
- /** Valid certificate URL for US Gov Cloud region used in tests. */
- private static final String VALID_CERT_URL_GOV_CLOUD = "https://sns.us-gov-west-1.amazonaws.com/cert.pem";
-
- /** Valid certificate URL for China region used in tests. */
- private static final String VALID_CERT_URL_CHINA = "https://sns.cn-north-1.amazonaws.com.cn/cert.pem";
-
- /**
- * Valid PEM-encoded X.509 certificate used for testing certificate parsing and validation.
- * This is a minimal test certificate that passes basic format validation.
- */
- private static final String VALID_PEM_CERTIFICATE =
- "-----BEGIN CERTIFICATE-----\n" +
- "MIIBkTCB+wIJAKZV5i2qhHcmMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNVBAMMCWxv\n" +
- "Y2FsaG9zdDAeFw0yMzAxMDEwMDAwMDBaFw0yNDAxMDEwMDAwMDBaMBQxEjAQBgNV\n" +
- "BAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAuVqVeII=\n" +
- "-----END CERTIFICATE-----";
-
- /**
- * Valid DER-encoded X.509 certificate used for testing binary certificate format handling.
- * This represents the same certificate as {@link #VALID_PEM_CERTIFICATE} in DER format.
- */
- private static final byte[] VALID_DER_CERTIFICATE = {
- (byte) 0x30, (byte) 0x82, (byte) 0x01, (byte) 0x91, (byte) 0x30, (byte) 0x82, (byte) 0x01, (byte) 0x3A,
- (byte) 0x02, (byte) 0x09, (byte) 0x00, (byte) 0xA6, (byte) 0x55, (byte) 0xE6, (byte) 0x2D, (byte) 0xAA,
- (byte) 0x84, (byte) 0x77, (byte) 0x26, (byte) 0x30, (byte) 0x0D, (byte) 0x06, (byte) 0x09, (byte) 0x2A,
- (byte) 0x86, (byte) 0x48, (byte) 0x86, (byte) 0xF7, (byte) 0x0D, (byte) 0x01, (byte) 0x01, (byte) 0x05,
- (byte) 0x05, (byte) 0x00, (byte) 0x30, (byte) 0x14, (byte) 0x31, (byte) 0x12, (byte) 0x30, (byte) 0x10,
- (byte) 0x06, (byte) 0x03, (byte) 0x55, (byte) 0x04, (byte) 0x03, (byte) 0x0C, (byte) 0x09, (byte) 0x6C,
- (byte) 0x6F, (byte) 0x63, (byte) 0x61, (byte) 0x6C, (byte) 0x68, (byte) 0x6F, (byte) 0x73, (byte) 0x74,
- (byte) 0x30, (byte) 0x1E, (byte) 0x17, (byte) 0x0D, (byte) 0x32, (byte) 0x33, (byte) 0x30, (byte) 0x31,
- (byte) 0x30, (byte) 0x31, (byte) 0x30, (byte) 0x30, (byte) 0x30, (byte) 0x30, (byte) 0x30, (byte) 0x30,
- (byte) 0x5A, (byte) 0x17, (byte) 0x0D, (byte) 0x32, (byte) 0x34, (byte) 0x30, (byte) 0x31, (byte) 0x30,
- (byte) 0x31, (byte) 0x30, (byte) 0x30, (byte) 0x30, (byte) 0x30, (byte) 0x30, (byte) 0x30, (byte) 0x5A,
- (byte) 0x30, (byte) 0x14, (byte) 0x31, (byte) 0x12, (byte) 0x30, (byte) 0x10, (byte) 0x06, (byte) 0x03,
- (byte) 0x55, (byte) 0x04, (byte) 0x03, (byte) 0x0C, (byte) 0x09, (byte) 0x6C, (byte) 0x6F, (byte) 0x63,
- (byte) 0x61, (byte) 0x6C, (byte) 0x68, (byte) 0x6F, (byte) 0x73, (byte) 0x74, (byte) 0x30, (byte) 0x81,
- (byte) 0x9F, (byte) 0x30, (byte) 0x0D, (byte) 0x06, (byte) 0x09, (byte) 0x2A, (byte) 0x86, (byte) 0x48,
- (byte) 0x86, (byte) 0xF7, (byte) 0x0D, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x05, (byte) 0x00,
- (byte) 0x03, (byte) 0x81, (byte) 0x8D, (byte) 0x00, (byte) 0x30, (byte) 0x81, (byte) 0x89, (byte) 0x02,
- (byte) 0x81, (byte) 0x81, (byte) 0x00, (byte) 0xB9, (byte) 0x5A, (byte) 0x95, (byte) 0x78, (byte) 0x82
- };
-
- /** Mock HTTP client used for testing certificate retrieval operations. */
- private SdkHttpClient mockHttpClient;
-
- /** Certificate retriever instance under test. */
- private CertificateRetriever certificateRetriever;
-
- /**
- * Sets up test fixtures before each test method execution.
- *
- *
- *
- *
- * @param validUrl A valid certificate URL from a trusted SNS domain
- * @throws Exception If certificate retrieval fails unexpectedly
- */
- @ParameterizedTest
- @ValueSource(strings = {
- "https://sns.us-east-1.amazonaws.com/cert.pem",
- "https://sns.eu-west-1.amazonaws.com/cert.pem",
- "https://sns.ap-southeast-2.amazonaws.com/cert.pem",
- "https://sns.us-gov-west-1.amazonaws.com/cert.pem",
- "https://sns.us-gov-east-1.amazonaws.com/cert.pem",
- "https://sns.cn-north-1.amazonaws.com.cn/cert.pem",
- "https://sns.cn-northwest-1.amazonaws.com.cn/cert.pem"
- })
- void retrieveCertificate_validTrustedDomains_acceptsUrl(String validUrl) throws Exception {
- setupSuccessfulHttpResponse(VALID_PEM_CERTIFICATE.getBytes(StandardCharsets.UTF_8));
-
- byte[] result = certificateRetriever.retrieveCertificate(validUrl);
-
- assertThat(result).isNotNull();
- assertThat(new String(result, StandardCharsets.UTF_8)).contains("-----BEGIN CERTIFICATE-----");
- }
-
- /**
- * Tests that certificate retrieval rejects URLs from various untrusted domains.
- *
- *
- *
- *
- * @param invalidUrl An invalid certificate URL from an untrusted domain
- * @throws SnsCertificateException Expected exception when URL is from untrusted domain
- */
- @ParameterizedTest
- @ValueSource(strings = {
- "https://fake-sns.us-east-1.amazonaws.com/cert.pem",
- "https://sns.us-east-1.fake.com/cert.pem",
- "https://sns.us-east-1.amazonaws.com.fake/cert.pem",
- "https://malicious.amazonaws.com/cert.pem",
- "https://sns..amazonaws.com/cert.pem",
- "https://sns.us-east-1-.amazonaws.com/cert.pem",
- "https://sns.-us-east-1.amazonaws.com/cert.pem"
- })
- void retrieveCertificate_invalidTrustedDomains_throwsException(String invalidUrl) {
- assertThatThrownBy(() -> certificateRetriever.retrieveCertificate(invalidUrl))
- .isInstanceOf(SnsCertificateException.class)
- .hasMessageContaining("Certificate URL is not from a trusted SNS domain");
- }
-
- @ParameterizedTest
- @ValueSource(strings = {
- "https://sns.fake-region.amazonaws.com/cert.pem"
- })
- void retrieveCertificate_validFormatButInvalidRegion_throwsException(String invalidUrl) {
- // These URLs pass the domain pattern validation but fail during HTTP request
- assertThatThrownBy(() -> certificateRetriever.retrieveCertificate(invalidUrl))
- .isInstanceOf(SnsCertificateException.class)
- .hasMessageContaining("Unexpected error while retrieving certificate");
- }
-
- // ========== HTTP Response and Network Error Tests ==========
-
- /**
- * Tests that certificate retrieval handles HTTP error responses appropriately.
- *
- *
- *
- *
- * @throws Exception If certificate retrieval fails unexpectedly
- */
- @Test
- void retrieveCertificate_cacheHit_returnsFromCache() throws Exception {
- setupSuccessfulHttpResponse(VALID_PEM_CERTIFICATE.getBytes(StandardCharsets.UTF_8));
-
- // First call should fetch from HTTP
- byte[] result1 = certificateRetriever.retrieveCertificate(VALID_CERT_URL_US_EAST_1);
-
- // Second call should return from cache without HTTP call
- byte[] result2 = certificateRetriever.retrieveCertificate(VALID_CERT_URL_US_EAST_1);
-
- assertThat(result1).isEqualTo(result2);
- // Verify HTTP client was called only once
- verify(mockHttpClient, times(1)).prepareRequest(any(HttpExecuteRequest.class));
- }
-
- @Test
- void retrieveCertificate_differentUrls_cachesIndependently() throws Exception {
- setupSuccessfulHttpResponse(VALID_PEM_CERTIFICATE.getBytes(StandardCharsets.UTF_8));
-
- // Retrieve certificates from different URLs
- byte[] result1 = certificateRetriever.retrieveCertificate(VALID_CERT_URL_US_EAST_1);
- byte[] result2 = certificateRetriever.retrieveCertificate(VALID_CERT_URL_EU_WEST_1);
-
- assertThat(result1).isEqualTo(result2); // Same content but different cache entries
- // Verify HTTP client was called twice (once for each URL)
- verify(mockHttpClient, times(2)).prepareRequest(any(HttpExecuteRequest.class));
-
- // Verify cache has both entries
- assertThat(certificateRetriever.getCacheSize()).isEqualTo(2);
- }
-
- @Test
- void retrieveCertificate_expiredCache_refetchesCertificate() throws Exception {
- // Create retriever with very short cache timeout
- CertificateRetriever shortCacheRetriever = new CertificateRetriever(mockHttpClient, Duration.ofMillis(10));
- setupSuccessfulHttpResponse(VALID_PEM_CERTIFICATE.getBytes(StandardCharsets.UTF_8));
-
- // First call
- byte[] result1 = shortCacheRetriever.retrieveCertificate(VALID_CERT_URL_US_EAST_1);
-
- // Wait for cache to expire
- Thread.sleep(20);
-
- // Second call should refetch
- byte[] result2 = shortCacheRetriever.retrieveCertificate(VALID_CERT_URL_US_EAST_1);
-
- assertThat(result1).isEqualTo(result2);
- // Verify HTTP client was called twice due to cache expiration
- verify(mockHttpClient, times(2)).prepareRequest(any(HttpExecuteRequest.class));
- }
-
- @Test
- void clearCache_removesAllCachedCertificates() throws Exception {
- setupSuccessfulHttpResponse(VALID_PEM_CERTIFICATE.getBytes(StandardCharsets.UTF_8));
-
- // Cache some certificates
- certificateRetriever.retrieveCertificate(VALID_CERT_URL_US_EAST_1);
- certificateRetriever.retrieveCertificate(VALID_CERT_URL_EU_WEST_1);
-
- assertThat(certificateRetriever.getCacheSize()).isEqualTo(2);
-
- // Clear cache
- certificateRetriever.clearCache();
-
- assertThat(certificateRetriever.getCacheSize()).isEqualTo(0);
- }
-
- @Test
- void getCacheSize_returnsCorrectSize() throws Exception {
- setupSuccessfulHttpResponse(VALID_PEM_CERTIFICATE.getBytes(StandardCharsets.UTF_8));
-
- assertThat(certificateRetriever.getCacheSize()).isEqualTo(0);
-
- certificateRetriever.retrieveCertificate(VALID_CERT_URL_US_EAST_1);
- assertThat(certificateRetriever.getCacheSize()).isEqualTo(1);
-
- certificateRetriever.retrieveCertificate(VALID_CERT_URL_EU_WEST_1);
- assertThat(certificateRetriever.getCacheSize()).isEqualTo(2);
-
- // Same URL should not increase cache size
- certificateRetriever.retrieveCertificate(VALID_CERT_URL_US_EAST_1);
- assertThat(certificateRetriever.getCacheSize()).isEqualTo(2);
- }
-
- // Thread-safety tests
- @Test
- void retrieveCertificate_concurrentAccess_threadSafe() throws Exception {
- setupSuccessfulHttpResponse(VALID_PEM_CERTIFICATE.getBytes(StandardCharsets.UTF_8));
-
- int threadCount = 10;
- ExecutorService executor = Executors.newFixedThreadPool(threadCount);
- CountDownLatch startLatch = new CountDownLatch(1);
- CountDownLatch completionLatch = new CountDownLatch(threadCount);
- AtomicInteger successCount = new AtomicInteger(0);
- AtomicInteger errorCount = new AtomicInteger(0);
-
- // Submit concurrent tasks
- for (int i = 0; i < threadCount; i++) {
- executor.submit(() -> {
- try {
- startLatch.await(); // Wait for all threads to be ready
- byte[] result = certificateRetriever.retrieveCertificate(VALID_CERT_URL_US_EAST_1);
- if (result != null && result.length > 0) {
- successCount.incrementAndGet();
- }
- } catch (Exception e) {
- errorCount.incrementAndGet();
- } finally {
- completionLatch.countDown();
- }
- });
- }
-
- // Start all threads simultaneously
- startLatch.countDown();
-
- // Wait for all threads to complete
- boolean completed = completionLatch.await(5, TimeUnit.SECONDS);
-
- assertThat(completed).isTrue();
- assertThat(successCount.get()).isEqualTo(threadCount);
- assertThat(errorCount.get()).isEqualTo(0);
-
- // Verify cache is thread-safe and contains only one entry
- assertThat(certificateRetriever.getCacheSize()).isEqualTo(1);
-
- executor.shutdown();
- }
-
- @Test
- void retrieveCertificate_concurrentDifferentUrls_threadSafe() throws Exception {
- setupSuccessfulHttpResponse(VALID_PEM_CERTIFICATE.getBytes(StandardCharsets.UTF_8));
-
- int threadCount = 20;
- ExecutorService executor = Executors.newFixedThreadPool(threadCount);
- CountDownLatch startLatch = new CountDownLatch(1);
- CountDownLatch completionLatch = new CountDownLatch(threadCount);
- AtomicInteger successCount = new AtomicInteger(0);
-
- String[] urls = {
- VALID_CERT_URL_US_EAST_1,
- VALID_CERT_URL_EU_WEST_1,
- VALID_CERT_URL_GOV_CLOUD,
- VALID_CERT_URL_CHINA
- };
-
- // Submit concurrent tasks with different URLs
- for (int i = 0; i < threadCount; i++) {
- final String url = urls[i % urls.length];
- executor.submit(() -> {
- try {
- startLatch.await();
- byte[] result = certificateRetriever.retrieveCertificate(url);
- if (result != null && result.length > 0) {
- successCount.incrementAndGet();
- }
- } catch (Exception e) {
- // Ignore for this test
- } finally {
- completionLatch.countDown();
- }
- });
- }
-
- startLatch.countDown();
- boolean completed = completionLatch.await(5, TimeUnit.SECONDS);
-
- assertThat(completed).isTrue();
- assertThat(successCount.get()).isEqualTo(threadCount);
-
- // Should have cached all unique URLs
- assertThat(certificateRetriever.getCacheSize()).isEqualTo(urls.length);
-
- executor.shutdown();
- }
-
- @Test
- void retrieveCertificate_concurrentCacheExpiration_threadSafe() throws Exception {
- // Create retriever with short cache timeout for this test
- CertificateRetriever shortCacheRetriever = new CertificateRetriever(mockHttpClient, Duration.ofMillis(50));
- setupSuccessfulHttpResponse(VALID_PEM_CERTIFICATE.getBytes(StandardCharsets.UTF_8));
-
- int threadCount = 10;
- ExecutorService executor = Executors.newFixedThreadPool(threadCount);
- AtomicInteger successCount = new AtomicInteger(0);
-
- // Submit tasks that will run over time to test cache expiration
- for (int i = 0; i < threadCount; i++) {
- final int delay = i * 10; // Stagger the requests
- executor.submit(() -> {
- try {
- Thread.sleep(delay);
- byte[] result = shortCacheRetriever.retrieveCertificate(VALID_CERT_URL_US_EAST_1);
- if (result != null && result.length > 0) {
- successCount.incrementAndGet();
- }
- } catch (Exception e) {
- // Ignore for this test
- }
- });
- }
-
- executor.shutdown();
- boolean terminated = executor.awaitTermination(2, TimeUnit.SECONDS);
-
- assertThat(terminated).isTrue();
- assertThat(successCount.get()).isEqualTo(threadCount);
- }
-
- // ========== Test Helper Methods ==========
-
- /**
- * Sets up a successful HTTP response mock for certificate retrieval testing.
- *
- *
- *
- *
- * @param certificateBytes The certificate data to return in the HTTP response body
- * @throws Exception If there are issues setting up the mock HTTP response
- */
- private void setupSuccessfulHttpResponse(byte[] certificateBytes) throws Exception {
- SdkHttpResponse successResponse = SdkHttpResponse.builder()
- .statusCode(200)
- .build();
-
- HttpExecuteResponse httpResponse = mock(HttpExecuteResponse.class);
- when(httpResponse.httpResponse()).thenReturn(successResponse);
- // Create a new stream for each call to handle concurrent access
- when(httpResponse.responseBody()).thenAnswer(invocation ->
- Optional.of(AbortableInputStream.create(new ByteArrayInputStream(certificateBytes))));
-
- ExecutableHttpRequest executableRequest = mock(ExecutableHttpRequest.class);
- when(executableRequest.call()).thenReturn(httpResponse);
-
- // Make the mock thread-safe by using lenient stubbing
- lenient().when(mockHttpClient.prepareRequest(any(HttpExecuteRequest.class)))
- .thenReturn(executableRequest);
- }
-}
\ No newline at end of file
diff --git a/services/sns/src/test/java/software/amazon/awssdk/services/sns/internal/messagemanager/SignatureValidatorTest.java b/services/sns/src/test/java/software/amazon/awssdk/services/sns/internal/messagemanager/SignatureValidatorTest.java
deleted file mode 100644
index 821ad7813aca..000000000000
--- a/services/sns/src/test/java/software/amazon/awssdk/services/sns/internal/messagemanager/SignatureValidatorTest.java
+++ /dev/null
@@ -1,375 +0,0 @@
-/*
- * 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.services.sns.internal.messagemanager;
-
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-
-import java.nio.charset.StandardCharsets;
-import java.time.Instant;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.ValueSource;
-import software.amazon.awssdk.services.sns.messagemanager.SnsCertificateException;
-import software.amazon.awssdk.services.sns.messagemanager.SnsMessage;
-import software.amazon.awssdk.services.sns.messagemanager.SnsSignatureValidationException;
-
-/**
- * Unit tests for {@link SignatureValidator}.
- *
- *
- *
- *
- *