Skip to main content

Module webhooks

Module webhooks 

Source
Expand description

Kick’s official webhooks: the headers, the signature, the bodies.

A delivery is a POST with the event in the body and the rest in headers (Headers::from_pairs). Kick signs <message id>.<timestamp>.<raw body> with RSA (SHA-256, PKCS#1 v1.5); verify checks it against the key from crate::public_api::public_key_request, read once into a PublicKey.

What a valid signature means, and what it does not:

  • Kick sent this body, at that timestamp, under that message id, to some app. Not necessarily to yours: every app gets the same key, so anyone who receives deliveries can replay one to you. Only the message id, the timestamp and the body are signed.
  • Replays: verify refuses a timestamp more than DEFAULT_TOLERANCE_SECS (5 minutes) from now (Freshness::at), and the receiver should also keep the message ids it has seen for at least that long and drop repeats - which Kick sends anyway (below). Freshness::Skip is for re-checking deliveries stored earlier.
  • The type, version and subscription-id headers are not signed. parse reads the body by the type header and refuses a body that lacks that type’s own field, so a delivery cannot be passed off as another kind (no known type’s body has another known type’s own field). Fields it does not know are ignored, so a field Kick adds later does not break deliveries. Two cases it cannot tell apart: channel.subscription.new and .renewal have one shape, and a type this crate does not know comes out as WebhookEvent::Other with the header’s claim unchecked.
  • The message id must be a ULID and the timestamp RFC 3339 (WebhookError::MalformedHeader). Neither can then hold the . that separates them in the signed text, so no header can move bytes between the id, the timestamp and the body.

What to expect (docs/KICK.md §4): the same event can arrive twice (key on the message id); a failed delivery has not been seen to be retried, so treat it as lost; metadata and follow bodies carry no time (use the timestamp header); a renewal’s created_at is when the subscription first started.

Retries and freshness. Kick’s docs imply retries “for over a day” though none has been seen. If Kick does retry, a retry very likely keeps the original message id and timestamp (they are what is signed), so one that comes more than the tolerance later is refused as WebhookError::Stale. The default (5 minutes) suits a receiver that is up: live deliveries arrive within a second or two, and a short window keeps the set of message ids to remember small. A receiver that would rather take late retries than lose them can pass a longer tolerance (Freshness::Within), up to a day or more, and must then remember message ids for that long, since the window is also how long a captured delivery can be replayed.

The RSA here is the rsa crate’s, used for verifying only. RUSTSEC-2023-0071 (the Marvin timing attack) is about operations with the private key, which this crate never has; see deny.toml.

Structs§

Category
A category in a metadata update.
Headers
The headers of one delivery.
Person
Someone in a webhook body: broadcaster, follower, subscriber, gifter, giftee, sender.
PublicKey
Kick’s webhook key, read once.

Enums§

Freshness
Whether, and how, verify checks the delivery’s timestamp.
WebhookError
Why a delivery was refused.
WebhookEvent
One webhook event, typed.

Constants§

DEFAULT_TOLERANCE_SECS
How far a delivery’s timestamp may be from now, either way, by default: 5 minutes. Kick’s deliveries arrive within a second or two; a retry, if Kick sends one, may carry the original timestamp and be refused as stale (see the module docs on choosing a tolerance).

Functions§

parse
Reads a delivery’s body (raw bytes or text) by its type header.
verify
Checks a delivery: the headers’ formats, the timestamp’s freshness, and the signature. body is the raw body, byte for byte (&[u8], &str, Vec<u8>, …): not re-encoded, and not necessarily UTF-8.
verify_pem
verify with the key as PEM, read on every call. A receiver checking many deliveries should read it once with PublicKey::from_pem.