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
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".
@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).
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.