Remove inbound request signature verification (closes #279)
All checks were successful
check / check (push) Successful in 3m10s

The receiver verified an optional per-entrypoint HMAC or shared token
before accepting a request. That is removed outright: the entrypoint
UUID in the URL is the authentication secret, and possession of it
authorises submission. This reverses the feature added in fcead5d.

Deletes the internal/signature package, the signature_scheme and
signature_secret columns from Entrypoint along with their accessors,
the per-entrypoint secret form and its POST route, and the scheme
labelling in EntrypointView. Pre-1.0 with no installed base, so the
columns simply stop being written; there is no migration and no
compatibility path.

Header sanitisation goes with it. SanitizeHeaders existed to strip a
scheme's own credential header before the header map was stored and
forwarded; with no configured credential there is nothing to strip, so
the receiver marshals the headers as received.

The receiver's other protections are untouched: the 1 MB body cap, the
per-IP rate limiter, the 410 for a deactivated entrypoint and the 404
for an unknown UUID.
This commit is contained in:
2026-08-24 01:12:14 +00:00
parent 032f265d69
commit cf48b465a6
16 changed files with 59 additions and 2227 deletions

View File

@@ -2,56 +2,18 @@ package handlers
import (
"sneak.berlin/go/webhooker/internal/database"
"sneak.berlin/go/webhooker/internal/signature"
)
// signatureUnavailable is what an entrypoint's scheme renders as when
// the stored value is not one this build supports. The stored string
// is never echoed as a fallback: it is operator-supplied and the row
// is already in a state the receiver refuses, so the UI says so
// rather than inventing a description for it.
const signatureUnavailable = "(unavailable)"
// signatureNotVerified is the label for an entrypoint that performs
// no inbound verification.
const signatureNotVerified = "not verified"
// signatureMisconfigured is the label for a row holding one half of
// the scheme/secret pair. The receiver answers every request to such
// an entrypoint 500, so calling it "not verified" would describe a
// receiver that is refusing everything as one that is accepting
// everything. The form cannot create the state; a hand-edited
// database or a downgrade past a scheme can.
const signatureMisconfigured = "misconfigured"
// EntrypointView is the display-safe projection of an entrypoint for
// the UI. It deliberately has no secret field, so no template —
// present or future — can render the shared secret, in the same way
// delivery.TargetView keeps a target's stored credential away from
// one.
// the UI, in the same way delivery.TargetView is one for a target.
type EntrypointView struct {
ID string
Path string
Description string
Active bool
// Configured reports whether inbound requests to this entrypoint
// are verified.
Configured bool
// Scheme is the stored scheme, carried so the form can preselect
// it. It names an algorithm, not a secret.
Scheme database.SignatureScheme
// SchemeLabel and SchemeHeader describe the configured scheme for
// display: the sender's name, and the header its signature
// arrives in.
SchemeLabel string
SchemeHeader string
}
// NewEntrypointViews projects entrypoints for rendering, dropping the
// shared secret on the way.
// NewEntrypointViews projects entrypoints for rendering.
func NewEntrypointViews(
entrypoints []database.Entrypoint,
) []EntrypointView {
@@ -60,31 +22,12 @@ func NewEntrypointViews(
for i := range entrypoints {
e := &entrypoints[i]
view := EntrypointView{
ID: e.ID,
Path: e.Path,
Description: e.Description,
Active: e.Active,
Configured: e.SignatureConfigured(),
Scheme: e.SignatureScheme,
SchemeLabel: signatureNotVerified,
SchemeHeader: "",
}
switch {
case view.Configured:
view.SchemeLabel = signatureUnavailable
info, ok := signature.Info(e.SignatureScheme)
if ok {
view.SchemeLabel = info.Label
view.SchemeHeader = info.Header
}
case e.SignatureHalfConfigured():
view.SchemeLabel = signatureMisconfigured
}
views = append(views, view)
views = append(views, EntrypointView{
ID: e.ID,
Path: e.Path,
Description: e.Description,
Active: e.Active,
})
}
return views