Skip to main content

Module webhook

Module webhook 

Source
Expand description

Outbound webhook delivery signatures: the sshsig over each delivered body. Outbound webhook delivery signatures (the event stream, the outbound leg).

A webhook is a subscriber to the event bus that POSTs each event to a configured URL. This module is the part that makes gunnar’s webhook worth more than a plain POST: every delivery is signed, so the receiver can prove the body came from this gunnar and was not forged or replayed by anything that merely knows the URL.

§Why sshsig, and why its own namespace

The signature is an sshsig — the same SSHSIG format ssh-keygen -Y sign produces and ssh-keygen -Y verify checks — exactly as the build record (crate::provenance), the forge event (crate::forge) and the signed tree head (crate::transparency) are. gunnar signs everything this way, and a webhook is not a place to invent a second scheme (one implementation).

The receiver verifies with the server’s published public key and no shared secret: a leaked delivery URL is not a leaked signing key, and a receiver behind a CDN or a queue can still tell a real delivery from a replayed or forged one. That is the property an HMAC shared secret — the GitHub model — cannot give, because with HMAC the verifier holds the same secret the signer does and anything that can verify can also forge.

The namespace is WEBHOOK_NAMESPACE, distinct from git, gunnar-provenance, gunnar-grpc and gunnar-transparency. sshsig binds the namespace into what is signed, so a webhook delivery signature can never be lifted and presented as a commit signature, a build attestation or a control-plane credential, and none of those can be presented as a webhook delivery. A signature is scoped to the one job it was made for.

§What is signed

The exact bytes of the request body — the JSON the receiver reads off the socket, byte for byte. Not a re-serialisation the receiver has to reproduce (JSON has no canonical form, so signing the meaning rather than the bytes would make the signature depend on the receiver’s serialiser), and not a digest the receiver has to trust the sender computed. The verifier hashes the body it actually received and checks the signature over that, so a single flipped byte anywhere in transit fails the check.

Structs§

SignedDelivery
A signed delivery: the fingerprint of the signing key and the sshsig.

Enums§

DeliveryProblem
What went wrong verifying a delivery. The variants a receiver distinguishes.

Constants§

DELIVERY_HEADER
The header carrying a per-delivery id (<epoch>:<seq>), so a receiver can deduplicate a redelivered event.
EVENT_HEADER
The header naming the event kind, e.g. ref.updated. Lets a receiver route or drop before parsing the body.
HMAC_HEADER
The GitHub-compatible HMAC header, set only when an endpoint configures a shared secret. sha256=<hex> over the body. The sshsig above is the default and the one that does not require the receiver to hold a secret; this exists so a receiver written for GitHub’s webhooks can be pointed at gunnar unchanged.
KEY_FINGERPRINT_HEADER
The header carrying SHA256:…, the fingerprint of the key that signed — which of the server’s published keys to verify against.
NAMESPACE_HEADER
The header naming the sshsig namespace, so a receiver can build the ssh-keygen -Y verify -n <namespace> invocation without hard-coding it.
SIGNATURE_HEADER
The HTTP header carrying the base64 of the sshsig PEM.
WEBHOOK_NAMESPACE
The sshsig namespace an outbound webhook delivery is signed under.

Functions§

hmac_sha256_header
The GitHub-compatible X-Hub-Signature-256 value for body under secret: sha256=<hex> of HMAC-SHA256(secret, body).
sign_delivery
Sign a delivery body under WEBHOOK_NAMESPACE.
verify_delivery
Verify a signed delivery against a public key.