ExAwsSnsVerifier

CI Hex.pm Hex Docs Downloads 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.

No runtime dependencies โ€” uses :public_key for RSA verification and :httpc for certificate fetching.

Features

Installation

Add ex_aws_sns_verifier to your mix.exs:

def deps do
  [
    {:ex_aws_sns_verifier, "~> 0.2.3"}
  ]
end

Then run:

mix deps.get

Upgrading to plug_crypto 2.2.1

This release requires plug_crypto >= 2.2.1 to fix CVE-2026-105963.

If your application uses encrypted cookie sessions (Plug.Session with encryption_salt), Plug.Crypto 2.x encrypts new cookies with XChaCha20-Poly1305 and requires a 32-byte key:

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 failure

4. 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: # ...
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

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

License

MIT ยฉ Gustavo Ziaugra