# ExAwsSnsVerifier
[](https://github.com/GustavoZiaugra/ex_aws_sns_verifier/actions/workflows/ci.yml)
[](https://hex.pm/packages/ex_aws_sns_verifier)
[](https://hexdocs.pm/ex_aws_sns_verifier)
[](https://hex.pm/packages/ex_aws_sns_verifier)
[](https://github.com/GustavoZiaugra/ex_aws_sns_verifier/blob/main/LICENSE)
**Verify AWS SNS HTTPS message authenticity โ RSA-SHA256 signature verification for Elixir applications.**
ExAwsSnsVerifier validates `Notification`, `SubscriptionConfirmation`, and `UnsubscribeConfirmation` payloads sent via the AWS SNS HTTPS transport. It is the Elixir equivalent of Ruby's [`Aws::SNS::MessageVerifier`](https://docs.aws.amazon.com/sns/latest/dg/sns-verify-signature-of-message.html).
No runtime dependencies โ uses `:public_key` for RSA verification and `:httpc` for certificate fetching.
## Features
- ๐ **RSA-SHA256 verification** โ validates `SignatureVersion 2` signatures using the AWS signing certificate
- ๐ฌ **All message types** โ `Notification`, `SubscriptionConfirmation`, and `UnsubscribeConfirmation`
- ๐ก๏ธ **Topic ARN allowlist** โ restrict which topics are accepted
- ๐ **URL hardening** โ enforces HTTPS, host whitelist (`sns.<region>.amazonaws.com`), no credentials in URL, `.pem` extension for certs
- โฐ **Replay protection** โ configurable timestamp window (default: 1 hour)
- ๐๏ธ **Cert caching** โ `:persistent_term`-backed cache with 24-hour TTL
- ๐ **Pluggable HTTP client** โ swap in Tesla, Req, Finch, or any custom client
- ๐งฉ **Plug integration** โ `ExAwsSnsVerifier.Plug` for Phoenix / Plug pipelines
- โก **Consistent error handling** โ `verify/2` returns `{:ok, payload}` or `{:error, reason}`; `verify!/2` raises
- ๐ฆ **Zero runtime dependencies** โ no Jason, no HTTPoison, no extra baggage
- ๐งช **Fully tested** โ matrix across OTP 26โ28 and Elixir 1.16โ1.19
## Installation
Add `ex_aws_sns_verifier` to your `mix.exs`:
```elixir
def deps do
[
{:ex_aws_sns_verifier, "~> 0.2.0"}
]
end
```
Then run:
```bash
mix deps.get
```
## Quick Start
### 1. One-shot verification
Pass the raw JSON body and a set of allowed topic ARNs:
```elixir
raw_body = ~s({
"Type": "Notification",
"MessageId": "22b80b92-fdea-4c2c-8f9d-bdfb0c7bf324",
"TopicArn": "arn:aws:sns:us-east-1:123456789012:MyTopic",
"Message": "Hello from SNS!",
"Timestamp": "2026-05-13T12:00:00.000Z",
"SignatureVersion": "2",
"Signature": "...",
"SigningCertURL": "https://sns.us-east-1.amazonaws.com/...pem",
...
})
opts = [
allowed_topic_arns: ["arn:aws:sns:us-east-1:123456789012:MyTopic"]
]
{:ok, payload} = ExAwsSnsVerifier.verify(raw_body, opts)
```
### 2. With a Verifier struct (reusable config)
```elixir
verifier = ExAwsSnsVerifier.new(
allowed_topic_arns: ["arn:aws:sns:us-east-1:123456789012:MyTopic"],
timestamp_window_seconds: 300 # 5 minutes
)
{:ok, payload} = ExAwsSnsVerifier.verify(verifier, raw_body)
```
### 3. Raise on failure
```elixir
payload = ExAwsSnsVerifier.verify!(raw_body, opts)
# Raises ExAwsSnsVerifier.VerificationError on failure
```
### 4. With a Plug pipeline
```elixir
# In your router or endpoint
plug ExAwsSnsVerifier.Plug,
allowed_topic_arns: ["arn:aws:sns:us-east-1:123456789012:MyTopic"]
```
The plug reads the raw body, verifies the SNS signature, and assigns `{:ok, payload}` or `{:error, reason}` to `conn.assigns.sns_verification`. On failure, the connection is halted with `403`.
## Configuration
### Verifier options
| Option | Default | Description |
|--------|---------|-------------|
| `allowed_topic_arns` | *(required)* | List of allowed TopicArn values |
| `allowed_regions` | All commercial regions | List of AWS regions for SigningCertURL validation |
| `timestamp_window_seconds` | `3600` | Replay protection window in seconds |
| `http_client` | `ExAwsSnsVerifier.Cert.HttpClient` | Module implementing `get/1` for cert fetching |
| `cert_cache` | `ExAwsSnsVerifier.Cert.Cache` | Module implementing `get/1` and `put/2` |
### Custom HTTP client
Swap in your own HTTP client (Tesla, Req, Finch, etc.):
```elixir
defmodule MyApp.MyHttpClient do
@behaviour ExAwsSnsVerifier.Cert.HttpClientBehaviour
@impl true
def get(url) do
# Return {:ok, body} or {:error, reason}
Req.get!(url).body
end
end
verifier = ExAwsSnsVerifier.new(
allowed_topic_arns: ["..."],
http_client: MyApp.MyHttpClient
)
```
### Custom cert cache
```elixir
defmodule MyApp.MyCache do
@behaviour ExAwsSnsVerifier.Cert.CacheBehaviour # get/1, put/2
@impl true
def get(key), do: # ...
@impl true
def put(key, value), do: # ...
end
```
## Error Reasons
`verify/2` returns `{:error, reason}` with one of the following atoms:
| Error | Meaning |
|-------|---------|
| `:invalid_json` | Body is not valid JSON |
| `:unknown_message_type` | `Type` is not Notification/SubscriptionConfirmation/UnsubscribeConfirmation |
| `:missing_signature_version` | No `SignatureVersion` field |
| `:unsupported_signature_version` | Only Version 2 (SHA256) supported |
| `:missing_timestamp` | No `Timestamp` field |
| `:invalid_timestamp` | Timestamp is not valid ISO 8601 |
| `:timestamp_out_of_window` | Message is outside the replay window |
| `:missing_topic_arn` | No `TopicArn` field |
| `:no_allowed_topics` | Allowlist is empty |
| `:topic_not_allowed` | TopicArn not in allowlist |
| `:missing_signature` | No `Signature` field |
| `:invalid_signature_encoding` | Signature is not valid Base64 |
| `:invalid_cert_url` | SigningCertURL failed host/path validation |
| `:missing_signing_cert_url` | No `SigningCertURL` field |
| `:signature_invalid` | RSA-SHA256 signature does not verify |
## How it works
1. **Parse** โ decodes the JSON body
2. **Validate type** โ confirms one of the three supported message types
3. **Validate signature version** โ only `SignatureVersion 2` (RSA-SHA256)
4. **Validate timestamp** โ checks the message is within the replay window
5. **Validate topic** โ confirms the `TopicArn` is in the allowlist
6. **Build canonical string** โ constructs the signed payload per AWS spec
7. **Fetch cert** โ downloads and caches the signing certificate from `SigningCertURL`
8. **Verify signature** โ RSA-SHA256 verification using `:public_key`
## Development
```bash
git clone https://github.com/GustavoZiaugra/ex_aws_sns_verifier.git
cd ex_aws_sns_verifier
mix deps.get
mix compile --warnings-as-errors
# Run tests
mix test
# Quality checks
mix format --check-formatted
mix credo --strict
mix dialyzer
# Generate docs
mix docs
```
## Links
- [AWS SNS Message Verification Docs](https://docs.aws.amazon.com/sns/latest/dg/sns-verify-signature-of-message.html)
- [Hex.pm](https://hex.pm/packages/ex_aws_sns_verifier)
- [HexDocs](https://hexdocs.pm/ex_aws_sns_verifier)
## License
MIT ยฉ [Gustavo Ziaugra](https://github.com/GustavoZiaugra)