From 7cf296bcee956570e666de44c1d1c2efa354b034 Mon Sep 17 00:00:00 2001 From: Chris Guidry Date: Thu, 05 Mar 2026 02:30:41 +0000 Subject: [PATCH] Make LookupVerification inherit from parent paths LookupSubscribeSecret already walks up the path hierarchy, but LookupVerification required an exact match. If you configured verification at `github.com`, a POST to `github.com/org/repo` would skip signature checking entirely. Now it uses the same walk-up loop. Co-Authored-By: Claude Opus 4.6 --- configuration.go | 14 ++++++++++---- configuration_test.go | 43 +++++++++++++++++++++++++++++++++++++------ server_test.go | 43 +++++++++++++++++++++++++++++++++++++++++++ 3 file(s) changed, 90 insertion(s)(+), 10 deletion(s)(-) diff --git a/configuration.go b/configuration.go --- a/configuration.go +++ b/configuration.go @@ -89,9 +89,15 @@ func (c *Configuration) LookupVerification(path string) *PathConfiguration { if c == nil { return nil } - pc, ok := c.Paths[path] - if !ok || pc.Verify == "" { - return nil + for { + if pc, ok := c.Paths[path]; ok && pc.Verify != "" { + return &pc + } + i := strings.LastIndex(path, "/") + if i < 0 { + break + } + path = path[:i] } - return &pc + return nil } diff --git a/configuration_test.go b/configuration_test.go --- a/configuration_test.go +++ b/configuration_test.go @@ -113,10 +113,10 @@ t.Errorf("expected empty string, got %s", secret) } } -func TestLookupPathConfiguration_exactMatchOnly(t *testing.T) { +func TestLookupVerification_inheritsFromParent(t *testing.T) { cfg := &Configuration{ Paths: map[string]PathConfiguration{ - "github.com/chrisguidry/docketeer": { + "github.com": { Verify: "hmac-sha256", Secret: "webhook-secret", SignatureHeader: "X-Hub-Signature-256", @@ -124,14 +124,45 @@ }, }, } - pc := cfg.LookupVerification("github.com/chrisguidry/docketeer") + pc := cfg.LookupVerification("github.com") if pc == nil { t.Fatal("expected path config for exact match") } - pc = cfg.LookupVerification("github.com/chrisguidry/docketeer/subpath") - if pc != nil { - t.Fatal("verification should not inherit from parent") + pc = cfg.LookupVerification("github.com/org/repo") + if pc == nil { + t.Fatal("expected verification to inherit from parent") + } + if pc.Verify != "hmac-sha256" { + t.Errorf("expected hmac-sha256, got %s", pc.Verify) + } +} + +func TestLookupVerification_childOverridesParent(t *testing.T) { + cfg := &Configuration{ + Paths: map[string]PathConfiguration{ + "github.com": { + Verify: "hmac-sha256", + Secret: "parent-secret", + SignatureHeader: "X-Hub-Signature-256", + }, + "github.com/org/repo": { + Verify: "hmac-sha1", + Secret: "child-secret", + SignatureHeader: "X-Hub-Signature", + }, + }, + } + + pc := cfg.LookupVerification("github.com/org/repo") + if pc == nil { + t.Fatal("expected path config for child") + } + if pc.Secret != "child-secret" { + t.Errorf("expected child-secret, got %s", pc.Secret) + } + if pc.Verify != "hmac-sha1" { + t.Errorf("expected hmac-sha1, got %s", pc.Verify) } } diff --git a/server_test.go b/server_test.go --- a/server_test.go +++ b/server_test.go @@ -281,6 +281,49 @@ t.Errorf("expected 500, got %d", w.code) } } +func TestServer_postInheritsVerificationFromParent(t *testing.T) { + cfg := &Configuration{ + Paths: map[string]PathConfiguration{ + "github.com": { + Verify: "hmac-sha256", + Secret: "parent-secret", + SignatureHeader: "X-Hub-Signature-256", + }, + }, + } + ts, _ := newTestServer(cfg) + defer ts.Close() + + body := `{"action":"push"}` + mac := hmac.New(sha256.New, []byte("parent-secret")) + mac.Write([]byte(body)) + sig := "sha256=" + hex.EncodeToString(mac.Sum(nil)) + + req, _ := http.NewRequest("POST", ts.URL+"/github.com/org/repo", strings.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Hub-Signature-256", sig) + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatalf("POST failed: %v", err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusAccepted { + t.Errorf("expected 202 with valid signature, got %d", resp.StatusCode) + } + + req, _ = http.NewRequest("POST", ts.URL+"/github.com/org/repo", strings.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Hub-Signature-256", "sha256=deadbeef") + resp, err = http.DefaultClient.Do(req) + if err != nil { + t.Fatalf("POST failed: %v", err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusForbidden { + t.Errorf("expected 403 with invalid signature, got %d", resp.StatusCode) + } +} + func TestServer_postWithMissingSignatureOnSecuredPath(t *testing.T) { cfg := &Configuration{ Paths: map[string]PathConfiguration{ -- tangled.sh