# `SquatchMail.SNS.MessageVerifier`
[🔗](https://github.com/Axio-Intelligence/Squatch-Mail/blob/v0.1.0/lib/squatch_mail/sns/message_verifier.ex#L1)

Hand-written verification of Amazon SNS message signatures.

SNS signs every HTTP(S) delivery (`Notification`, `SubscriptionConfirmation`,
`UnsubscribeConfirmation`) with a private key, and publishes the matching
X.509 certificate at a URL it includes in the message (`SigningCertURL`).
Verifying a message means: fetch that certificate (after validating the URL
actually points at AWS), extract its public key, rebuild the exact string
SNS signed, and check the signature against it with `:public_key.verify/4`.

This is intentionally dependency-free per `CLAUDE.md` - only `:public_key`,
`:crypto`, and `Finch` (already a dep, via the shared `SquatchMail.Finch`
pool) are used. No `ex_aws_sns`.

## Canonical string-to-sign

SNS signs a newline-delimited string built from a fixed subset of the
message's own fields, in a fixed order - not the raw JSON. The fields
differ by message `Type`:

  * `Notification` - `Message`, `MessageId`, `Subject` (only if present),
    `Timestamp`, `TopicArn`, `Type`.
  * `SubscriptionConfirmation` / `UnsubscribeConfirmation` - `Message`,
    `MessageId`, `SubscribeURL`, `Timestamp`, `Token`, `TopicArn`, `Type`.

Each field contributes two lines: its name, then its value (`"key\nvalue\n"`).
Fields absent from the message (i.e. `Subject` when there is none) are
skipped entirely rather than contributing an empty line - this is the
"trailing newline gotcha" from the AJ Foster writeup: it's not about a
newline at the very end, it's that every *present* field contributes
its own trailing newline, and omitting a field must not leave a blank
line in its place.

## Signature versions

`SignatureVersion` `"1"` uses SHA1withRSA, `"2"` uses SHA256withRSA. Both
are supported; verification dispatches on the message's own field.

## Escape hatch

Set `config :squatch_mail, verify_sns_signatures: false` to skip
verification entirely (tests, local dev without real SNS traffic). A
warning is logged every time this bypass is exercised so it can't be
silently left on in a real deployment.

# `message`

```elixir
@type message() :: %{optional(String.t()) =&gt; String.t()}
```

# `verify`

```elixir
@spec verify(message()) :: :ok | {:error, term()}
```

Verifies an SNS message's signature.

`message` is the JSON-decoded SNS envelope (string keys, as delivered)
containing at least `Type`, `Message`, `MessageId`, `Timestamp`,
`TopicArn`, `SignatureVersion`, `Signature`, and `SigningCertURL`, plus
`Subject` (Notification, optional) or `SubscribeURL`/`Token`
(SubscriptionConfirmation/UnsubscribeConfirmation).

Returns `:ok` or `{:error, reason}`. `reason` is an atom or string
describing what failed (missing field, bad cert host, expired cert,
signature mismatch, fetch failure, etc).

---

*Consult [api-reference.md](api-reference.md) for complete listing*
