Signature and timestamp verification. HMAC is microseconds, so it runs inline on the hot path, before the ack.
Adapters implement verify/2 over the raw envelope (the exact bytes matter).
opts carry the per-source secret and any tolerance window
(Ankusa.Verifier.check_timestamp/2).
The verifiers in core:
Ankusa.Verifier.None— accepts everything;Ankusa.Verifier.Hmac— a shared-secret signature over the request;Ankusa.Verifier.Asymmetric— a public-key (Ed25519, RSA) signature over the request; the node holds no secret;Ankusa.Verifier.Token— a static bearer, basic, header or query credential;Ankusa.Verifier.ClientCert— the client certificate of the edge listener's mutual-TLS handshake.
The edge puts request context some verifiers need into opts, for those
verifiers only: :peer_cert (the DER client certificate, or nil) for
ClientCert and :query (the raw query string) for a Token in the query.
Failure vocabulary
Adapters return {:error, reason}, which the source's on_verify_failure
policy turns into :reject, :quarantine, or :accept_flag. The reasons are
deliberately shared so telemetry and dashboards can count them without
knowing which provider produced them:
:missing_signature— a required header, credential or certificate is absent:malformed_signature— present, but not parseable:no_match— nothing matched the expected digest, key, token or pin:timestamp_out_of_tolerance— outside the replay window:bad_secret— the configured secret can't be used as a key (Verifier.Hmac,Verifier.Token):bad_key— a configured public key can't be used (Verifier.Asymmetric):bad_pin— a certificate source pins nothing usable (Verifier.ClientCert):bad_scheme— an unknown or missing scheme name (Verifier.Hmac,Verifier.Asymmetric), or an unknown:location(Verifier.Token):bad_tolerance—:toleranceis not an integer from 0 tomax_tolerance_seconds/0(Verifier.Hmac,Verifier.Asymmetric)
Summary
Callbacks
Human name of the scheme for telemetry; nil when the verifier has no named
scheme. Optional — verifiers that only implement verify/2 are attributed by
their module name instead.
Check opts without a request: :ok, or the {:error, reason} verify/2
would return for every hook. Optional — validate_config!/1 runs it, at boot,
for every source declared in config whose verifier implements it, and the
image's loader runs it for every source an admin API write submits.
Verify an envelope. Return :ok to accept, or {:error, reason} to trigger
the source's on_verify_failure policy (:reject | :quarantine | :accept_flag).
Functions
Whether source may run under config: {:error, message} when it is shared
(tenant_id "default"), has no verifier (Ankusa.Verifier.None), does not
set trust_url_tenant: true, and the edge takes the tenant from the request
(any Ankusa.RouteResolver but Ankusa.RouteResolver.Path). Such a source
would file hooks under whatever tenant the request names, and spend that
tenant's rate limit, with nothing to say the sender may. The verdict depends
on the config alone, never on the node's roles, so every node reading one
config agrees.
Check a Unix-seconds timestamp from a signature header against a tolerance window.
Whether opts[:tolerance] is a usable replay window: absent (the default
window), or an integer from 0 to max_tolerance_seconds/0. A window wider
than an hour is not a window, it is the absence of one. Shared by every
verifier that checks a timestamp.
The widest replay window, in seconds, a verifier accepts: an hour. Beyond that a signed hook stays replayable long enough that the window protects nothing.
The scheme name a verification is attributed to: mod.scheme_name(opts)
when the verifier implements it, else the module name.
Constant-time compare of two binaries of equal length.
Refuse to boot with a source declared in config whose verifier would fail
every hook, which on_verify_failure then quarantines, flags or rejects —
silently, at runtime. Every verifier that implements validate_opts/1 is
asked: an Ankusa.Verifier.Hmac or Ankusa.Verifier.Token secret that is
missing, empty or undecodable, an Ankusa.Verifier.Asymmetric public key that
is not a key, an Ankusa.Verifier.ClientCert that pins nothing, an unknown
scheme, or a :tolerance outside 0..max_tolerance_seconds/0. A
ClientCert source also needs listener.tls with verify: :verify_peer:
no other listener ever receives a client certificate. Sources a writable store
holds are checked when they are written. Raises ArgumentError naming the
source. A source that verifies with SHA-1
(Ankusa.Verifier.Hmac.weak_hash?/1) boots, with one warning.
Refuse to boot with a source declared in config that check_shared/2
refuses. Sources a writable store holds are checked when they are written
(and a stored one written before the check existed is named by
warn_stored_shared/2). Raises ArgumentError naming the source.
Log one warning per stored (API-managed) source that check_shared/2
refuses. Such a source was written before writes were checked: it keeps
serving as it did, and only a write that still lacks trust_url_tenant: true
is refused.
Whether the verifier {mod, opts} checks signatures with a weak hash (SHA-1).
Only Ankusa.Verifier.Hmac can; see Ankusa.Verifier.Hmac.weak_hash?/1.
Callbacks
Human name of the scheme for telemetry; nil when the verifier has no named
scheme. Optional — verifiers that only implement verify/2 are attributed by
their module name instead.
Check opts without a request: :ok, or the {:error, reason} verify/2
would return for every hook. Optional — validate_config!/1 runs it, at boot,
for every source declared in config whose verifier implements it, and the
image's loader runs it for every source an admin API write submits.
@callback verify(Ankusa.Envelope.t(), opts :: keyword()) :: :ok | {:error, term()}
Verify an envelope. Return :ok to accept, or {:error, reason} to trigger
the source's on_verify_failure policy (:reject | :quarantine | :accept_flag).
Functions
Check a Unix-seconds timestamp from a signature header against a tolerance window.
Every verifier that carries a signed timestamp needs this, so the window
semantics are defined once instead of per adapter. opts may set :tolerance
in seconds (default 300, i.e. ±5 minutes), and
:now (Unix seconds) to judge the window against a fixed instant instead of
the clock: Ankusa.Dispatch.Replayer sets it to the hook's receive time when
it re-verifies a quarantined hook, so a release hours later holds the hook to
the window it arrived in, not one it can no longer meet.
@spec check_tolerance(keyword()) :: :ok | {:error, :bad_tolerance}
Whether opts[:tolerance] is a usable replay window: absent (the default
window), or an integer from 0 to max_tolerance_seconds/0. A window wider
than an hour is not a window, it is the absence of one. Shared by every
verifier that checks a timestamp.
@spec max_tolerance_seconds() :: pos_integer()
The widest replay window, in seconds, a verifier accepts: an hour. Beyond that a signed hook stays replayable long enough that the window protects nothing.
The scheme name a verification is attributed to: mod.scheme_name(opts)
when the verifier implements it, else the module name.
Constant-time compare of two binaries of equal length.
@spec validate_config!(Ankusa.Config.t()) :: :ok
Refuse to boot with a source declared in config whose verifier would fail
every hook, which on_verify_failure then quarantines, flags or rejects —
silently, at runtime. Every verifier that implements validate_opts/1 is
asked: an Ankusa.Verifier.Hmac or Ankusa.Verifier.Token secret that is
missing, empty or undecodable, an Ankusa.Verifier.Asymmetric public key that
is not a key, an Ankusa.Verifier.ClientCert that pins nothing, an unknown
scheme, or a :tolerance outside 0..max_tolerance_seconds/0. A
ClientCert source also needs listener.tls with verify: :verify_peer:
no other listener ever receives a client certificate. Sources a writable store
holds are checked when they are written. Raises ArgumentError naming the
source. A source that verifies with SHA-1
(Ankusa.Verifier.Hmac.weak_hash?/1) boots, with one warning.
Whether the verifier {mod, opts} checks signatures with a weak hash (SHA-1).
Only Ankusa.Verifier.Hmac can; see Ankusa.Verifier.Hmac.weak_hash?/1.