Skip to content

SnsMessageManager fails signature validation for messages with a whole-second timestamp (.000 ms) #7010

@henricook

Description

@henricook

Describe the bug

SnsMessageManager throws The computed signature did not match the expected signature for a small but steady fraction (~1 in 1000) of genuine, Amazon-signed SNS messages. The messages are valid - the SDK's verification seems to have a bug.

When it rebuilds the string-to-sign, SignatureValidator.buildCanonicalMessage(...) emits the timestamp via notification.timestamp().toString(). SNS signs over the literal wire value, which always carries millisecond precision (...:ss.SSSZ), but Instant#toString() omits the fraction when it is zero. So any message whose Timestamp lands exactly on a second (...:ss.000Z) gets recomputed as ...:ssZ, the canonical strings differ, and verification fails. The same round-trip is used for the SubscriptionConfirmation and UnsubscribeConfirmation canonical strings.

This bit us processing SES delivery/open/bounce events over SNS - roughly 0.1% of notifications silently fail validation and get dropped, which is exactly the 1-in-1000 chance of the millisecond component being 000. Only the all-zero .000 case is affected; non-zero milliseconds (e.g. .001, .010, .100) round-trip fine because Instant#toString() pads those to three digits.

Regression Issue

No - present since the module was introduced.

Expected Behavior

A genuine, Amazon-signed SNS message validates regardless of whether its timestamp happens to fall on a whole second.

Current Behavior

parseMessage throws:

software.amazon.awssdk.core.exception.SdkClientException: The computed signature did not match the expected signature

Reproduction Steps

The defect is the lossy timestamp round-trip used to build the string-to-sign:

String onTheWire  = "2024-01-01T00:00:00.000Z";          // what SNS sends and signs
String recomputed = Instant.parse(onTheWire).toString();
System.out.println(recomputed);                           // 2024-01-01T00:00:00Z   <- ".000" dropped
System.out.println(onTheWire.equals(recomputed));         // false

Because buildCanonicalMessage feeds timestamp().toString() into the canonical string, the recomputed value differs from the bytes SNS signed whenever the milliseconds are 000, so signature.verify(...) returns false. Confirmed end-to-end against a real Amazon-signed message: it verifies against the raw .000Z timestamp and fails against the Instant#toString() form.

Possible Solution

Build the timestamp portion with fixed millisecond precision instead of Instant#toString(), e.g.

DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ROOT).withZone(ZoneOffset.UTC)

I have a fix plus a failing/passing test ready and am happy to open a PR.

AWS Java SDK version used

2.44.12 (also reproduces on 2.45.1)

JDK version used

21 (Azul Zulu 21.0.11)

Operating System and version

Linux

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions