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:
verifyrefuses a timestamp more thanDEFAULT_TOLERANCE_SECS(5 minutes) fromnow(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::Skipis for re-checking deliveries stored earlier. - The type, version and subscription-id headers are not signed.
parsereads 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.newand.renewalhave one shape, and a type this crate does not know comes out asWebhookEvent::Otherwith 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.
- Public
Key - Kick’s webhook key, read once.
Enums§
- Freshness
- Whether, and how,
verifychecks the delivery’s timestamp. - Webhook
Error - Why a delivery was refused.
- Webhook
Event - 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.
bodyis the raw body, byte for byte (&[u8],&str,Vec<u8>, …): not re-encoded, and not necessarily UTF-8. - verify_
pem verifywith the key as PEM, read on every call. A receiver checking many deliveries should read it once withPublicKey::from_pem.