A Webhook's Secret

What proves a post from outside comes from the sender that holds the secret, and the one way every receiver of Link's checks it.

What it proves

A post from outside (a connection's push, a payment's, a repository's, one to an agent) comes from the sender that holds the secret, as that sender signs: the secret echoed in a header (Telegram's), or an HMAC-SHA256 over parts of the post (Slack's, Stripe's, GitHub's, and Link's own).

How it's checked

One way in, link_secret_value::inbound, for every receiver: a receiver says how its sender signs, as a value (a Scheme), and never writes verification. An Inbound holds the scheme, the receiver's cap and its secrets; checking a post gives the post's id, or a Refused saying why not. Every receiver gets the same rules.

Capped, and closed by default

The body is capped before anything is read, each receiver stating its cap, with no default. A receiver without a secret refuses every post, and nothing builds one that takes anyone.

Compared in constant time

The secret or its HMAC (at most eight listed signatures read, each 32 bytes, against one HMAC per secret), which is link_secret_value::hmac, on ring, the one HMAC and the one hex decoder for a signature in every workspace (in the core, by architecture::only_link_secret_does_hmac; an OpenSSH known_hosts line's HMAC-SHA1, matched by lnk box, is no signature).

A signed time, and a replay refused

A signed time is taken only within its window, either side. A delivery's id, where the sender gives one, and always one it signed (GitHub signs only its body, so its id is the body's SHA-256, never its unsigned delivery header), is kept by the receiver's store (Seen) for its window, or for as long as the sender redelivers when it signs no time: LONGEST_KEEP, 30 days, GitHub's, the longest any sender redelivers.

Kept by name

It's kept under the receiver's name and its scheme's, so one sender's ids never stand for another's in a shared store; and a scheme that signs no time takes nothing with a store that forgets at a restart.

Any secret rolls with no restart

The current secret and the one before it are both tried, so the sender may switch at any moment, and a previous secret alone takes nothing.

One answer for each refusal

Each Refused has one answer, and a refusal is logged by its kind, never with the body.

503

When the receiver has no secret yet (Unconfigured) or its store can't say (Unavailable), so the sender retries.

413 and 400

413 for a body over the cap (TooBig), and 400 for a missing or unreadable time or id (Malformed).

401

For a missing or wrong signature (Unsigned, BadSignature), a stale time (Stale) or a replay (Replayed).

Scheme::LINK: an HMAC over link webhook, the time, the id and the body, each after a NUL, in the headers link-time, link-id and link-signature: v1=<hex>; a 300-second window, and the id required.