Ankusa.Verifier.Hmac (ankusa v0.7.0)

Copy Markdown View Source

Configurable HMAC signature verifier, driven by a %Ankusa.Verifier.Hmac.Scheme{} descriptor instead of one module per provider.

opts must carry :scheme — either a named preset atom (:stripe | :github | :standard_webhooks | :shopify | :slack, resolved through Ankusa.Verifier.Schemes) or an inline %Scheme{} describing any body-HMAC provider. :secret and :tolerance carry the key and replay window, exactly as with the old bespoke verifiers.

:secret is a binary or a list of binaries (current first); every key is tried, so a rotation window is secret: [new, old] — hooks signed with either verify while the provider switches over, and none land in quarantine. An empty key would verify whatever anyone signs with it, so a missing or empty secret, an empty list, or an element that is empty or does not decode fails every hook with :bad_secret.

:tolerance is the replay window in seconds, 0 to Ankusa.Verifier.max_tolerance_seconds/0 (3600; default 300). A window wider than an hour is not a window, so anything else fails every hook with :bad_tolerance and refuses the boot of a source declared in config.

hash: :sha1 on an inline scheme is allowed, for providers that still sign with it, but it is no longer collision resistant: weak_hash?/1 reports it, boot logs a warning, and every such verification counts toward ankusa_verifier_weak_hash_total.

Providers that are not body-HMAC — Twilio (SHA1 over the full request URL plus sorted form params) and PayPal (RSA over a certificate fetched from a provider URL) — are not expressible here and stay bespoke Ankusa.Verifier modules.

Summary

Functions

The HMAC key a configured secret stands for: the string itself (:raw), or the base64 after an optional whsec_ prefix (:whsec_base64, Standard Webhooks). :error when the base64 does not decode.

Human name of the scheme for telemetry: the preset name, or "custom".

Check opts the way verify/2 would use them, without a request: the scheme resolves, :tolerance (when set) is an integer from 0 to Ankusa.Verifier.max_tolerance_seconds/0, and every secret decodes to a non-empty key. :ok, or the error verify/2 would return for every hook. Used at boot for sources declared in config (Ankusa.Verifier.validate_config!/1).

Whether opts verify with SHA-1, a hash that is no longer collision resistant. No named preset uses it (verifier_test.exs asserts that), so only an inline %Scheme{hash: :sha1} is weak. It works, because some providers still sign that way; Ankusa.Verifier.validate_config!/1 logs a warning at boot and every verification increments ankusa_verifier_weak_hash_total.

Functions

raw_key(secret, atom)

@spec raw_key(String.t(), :raw | :whsec_base64) :: {:ok, binary()} | :error

The HMAC key a configured secret stands for: the string itself (:raw), or the base64 after an optional whsec_ prefix (:whsec_base64, Standard Webhooks). :error when the base64 does not decode.

scheme_name(opts)

@spec scheme_name(keyword()) :: String.t()

Human name of the scheme for telemetry: the preset name, or "custom".

validate_opts(opts)

@spec validate_opts(keyword()) ::
  :ok | {:error, :bad_secret | :bad_scheme | :bad_tolerance}

Check opts the way verify/2 would use them, without a request: the scheme resolves, :tolerance (when set) is an integer from 0 to Ankusa.Verifier.max_tolerance_seconds/0, and every secret decodes to a non-empty key. :ok, or the error verify/2 would return for every hook. Used at boot for sources declared in config (Ankusa.Verifier.validate_config!/1).

weak_hash?(opts)

@spec weak_hash?(keyword()) :: boolean()

Whether opts verify with SHA-1, a hash that is no longer collision resistant. No named preset uses it (verifier_test.exs asserts that), so only an inline %Scheme{hash: :sha1} is weak. It works, because some providers still sign that way; Ankusa.Verifier.validate_config!/1 logs a warning at boot and every verification increments ankusa_verifier_weak_hash_total.