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.
No runtime dependencies โ uses :public_key for RSA verification and :httpc for certificate fetching.
Features
- ๐ RSA-SHA256 verification โ validates
SignatureVersion 2signatures using the AWS signing certificate - ๐ฌ All message types โ
Notification,SubscriptionConfirmation, andUnsubscribeConfirmation - ๐ก๏ธ Topic ARN allowlist โ restrict which topics are accepted
- ๐ URL hardening โ enforces HTTPS, host whitelist (
sns.<region>.amazonaws.com), no credentials in URL,.pemextension 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.Plugfor Phoenix / Plug pipelines - โก Consistent error handling โ
verify/2returns{:ok, payload}or{:error, reason};verify!/2raises - ๐ฆ 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:
def deps do
[
{:ex_aws_sns_verifier, "~> 0.2.0"}
]
endThen run:
mix deps.get
Quick Start
1. One-shot verification
Pass the raw JSON body and a set of allowed topic ARNs:
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)
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
payload = ExAwsSnsVerifier.verify!(raw_body, opts)
# Raises ExAwsSnsVerifier.VerificationError on failure4. With a Plug pipeline
# 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.):
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
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: # ...
endError 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
- Parse โ decodes the JSON body
- Validate type โ confirms one of the three supported message types
- Validate signature version โ only
SignatureVersion 2(RSA-SHA256) - Validate timestamp โ checks the message is within the replay window
- Validate topic โ confirms the
TopicArnis in the allowlist - Build canonical string โ constructs the signed payload per AWS spec
- Fetch cert โ downloads and caches the signing certificate from
SigningCertURL - Verify signature โ RSA-SHA256 verification using
:public_key
Development
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
License
MIT ยฉ Gustavo Ziaugra