Remove inbound request signature verification (closes #279)
All checks were successful
check / check (push) Successful in 3m14s
All checks were successful
check / check (push) Successful in 3m14s
This commit was merged in pull request #281.
This commit is contained in:
@@ -13,7 +13,6 @@ import (
|
||||
"gorm.io/gorm"
|
||||
"sneak.berlin/go/webhooker/internal/database"
|
||||
"sneak.berlin/go/webhooker/internal/delivery"
|
||||
"sneak.berlin/go/webhooker/internal/signature"
|
||||
)
|
||||
|
||||
// WebhookListItem holds data for the webhook list view.
|
||||
@@ -443,16 +442,13 @@ func (h *Handlers) renderSourceDetail(
|
||||
// receivers; html/template cannot address a value stored in a map.
|
||||
data := map[string]any{
|
||||
tmplKeyWebhook: &webhook,
|
||||
// Entrypoints and targets are both projected to
|
||||
// display-safe views: an entrypoint carries the shared
|
||||
// secret its senders sign with and a target's stored
|
||||
// config blob holds a credential, and neither must ever
|
||||
// reach a template.
|
||||
"Entrypoints": NewEntrypointViews(entrypoints),
|
||||
"Targets": delivery.NewTargetViews(targets),
|
||||
"SignatureSchemes": signature.Schemes(),
|
||||
"Events": events,
|
||||
"BaseURL": scheme + "://" + host,
|
||||
// Targets are projected to a display-safe view: a
|
||||
// target's stored config blob holds a credential, and it
|
||||
// must never reach a template.
|
||||
"Entrypoints": NewEntrypointViews(entrypoints),
|
||||
"Targets": delivery.NewTargetViews(targets),
|
||||
"Events": events,
|
||||
"BaseURL": scheme + "://" + host,
|
||||
}
|
||||
|
||||
h.renderTemplate(w, r, "source_detail.html", data)
|
||||
@@ -1265,145 +1261,6 @@ func (h *Handlers) HandleEntrypointCreate() http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// HandleEntrypointSecret sets, rotates or removes the shared secret
|
||||
// an entrypoint verifies inbound requests with.
|
||||
//
|
||||
// Setting and rotating are the same operation: the form always takes
|
||||
// the secret afresh and the stored value is never sent to the browser
|
||||
// to be edited, so there is no path by which the page can display a
|
||||
// credential it holds. Rotation is therefore "submit the new secret",
|
||||
// and the operator already has that value — both supported senders
|
||||
// require them to enter the same string on the sender's side, so
|
||||
// there is no generated value for webhooker to reveal once.
|
||||
func (h *Handlers) HandleEntrypointSecret() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
webhook, ok := h.ownedWebhook(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// The body size cap is enforced by the MaxBodySize
|
||||
// middleware, which runs before CSRF parses the form.
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
http.Error(
|
||||
w, "Bad request", http.StatusBadRequest,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var entrypoint database.Entrypoint
|
||||
|
||||
err = h.db.DB().Where(
|
||||
"id = ? AND webhook_id = ?",
|
||||
chi.URLParam(r, "entrypointID"), webhook.ID,
|
||||
).First(&entrypoint).Error
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
h.applyEntrypointSecret(w, r, &entrypoint)
|
||||
}
|
||||
}
|
||||
|
||||
// applyEntrypointSecret validates the submitted scheme and secret and
|
||||
// stores them.
|
||||
//
|
||||
// A scheme this build does not support is a 400, never a stored value
|
||||
// the receiver would later have to interpret: the receiver fails such
|
||||
// a row closed, so letting one be created would take the entrypoint
|
||||
// offline through a form that reported success.
|
||||
func (h *Handlers) applyEntrypointSecret(
|
||||
w http.ResponseWriter,
|
||||
r *http.Request,
|
||||
entrypoint *database.Entrypoint,
|
||||
) {
|
||||
// PostFormValue, not FormValue: a credential must come from the
|
||||
// body. FormValue falls back to the query string, and the request
|
||||
// line — unlike the body — is what logs, proxies, Referer headers
|
||||
// and error trackers record.
|
||||
scheme := database.SignatureScheme(
|
||||
r.PostFormValue("signature_scheme"),
|
||||
)
|
||||
|
||||
// Surrounding whitespace is stripped, because a secret pasted from
|
||||
// a password manager routinely carries some and the resulting
|
||||
// mismatch is undiagnosable from the sender's side. A secret whose
|
||||
// own first or last character is a space cannot be stored; the
|
||||
// README says so.
|
||||
secret := strings.TrimSpace(r.PostFormValue("secret"))
|
||||
|
||||
if !signature.Supported(scheme) {
|
||||
http.Error(
|
||||
w, "Invalid signature scheme",
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if scheme == database.SignatureSchemeNone {
|
||||
// Turning verification off drops the secret with it: a stored
|
||||
// credential nothing reads is one more copy to leak, and
|
||||
// Verify refuses that pairing in any case.
|
||||
secret = ""
|
||||
} else if secret == "" {
|
||||
http.Error(
|
||||
w,
|
||||
"A shared secret is required for this signature scheme.",
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
h.storeEntrypointSecret(w, r, entrypoint, scheme, secret)
|
||||
}
|
||||
|
||||
// storeEntrypointSecret writes a validated scheme and secret to an
|
||||
// entrypoint and returns the operator to the webhook page.
|
||||
func (h *Handlers) storeEntrypointSecret(
|
||||
w http.ResponseWriter,
|
||||
r *http.Request,
|
||||
entrypoint *database.Entrypoint,
|
||||
scheme database.SignatureScheme,
|
||||
secret string,
|
||||
) {
|
||||
// Updates with a map rather than a struct: a struct update skips
|
||||
// zero values, and the empty pair is exactly what has to be
|
||||
// written when verification is being turned off.
|
||||
err := h.db.DB().Model(entrypoint).Updates(map[string]any{
|
||||
"signature_scheme": scheme,
|
||||
"signature_secret": secret,
|
||||
}).Error
|
||||
if err != nil {
|
||||
// The error is logged by serverError; GORM's error text
|
||||
// carries the statement, not the bound values, so the secret
|
||||
// does not travel with it.
|
||||
h.serverError(
|
||||
w, "failed to update entrypoint signature", err,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
h.log.Info(
|
||||
"entrypoint signature configuration updated",
|
||||
"entrypoint_id", entrypoint.ID,
|
||||
"webhook_id", entrypoint.WebhookID,
|
||||
"scheme", string(scheme),
|
||||
)
|
||||
|
||||
http.Redirect(
|
||||
w, r,
|
||||
"/source/"+entrypoint.WebhookID,
|
||||
http.StatusSeeOther,
|
||||
)
|
||||
}
|
||||
|
||||
// HandleTargetCreate handles adding a new target to a webhook.
|
||||
func (h *Handlers) HandleTargetCreate() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Reference in New Issue
Block a user