S33: HMAC-SHA256 sign + verify
- SHA
706426a394c97b6e5da10cb5d687ea1d22bb432b- Parents
-
4a9b2e1 - Tree
df5c738
706426a
706426a394c97b6e5da10cb5d687ea1d22bb432b4a9b2e1
df5c738| Status | File | + | - |
|---|---|---|---|
| A |
internal/webhook/sign.go
|
30 | 0 |
| A |
internal/webhook/sign_test.go
|
41 | 0 |
internal/webhook/sign.goadded@@ -0,0 +1,30 @@ | ||
| 1 | +// SPDX-License-Identifier: AGPL-3.0-or-later | |
| 2 | + | |
| 3 | +package webhook | |
| 4 | + | |
| 5 | +import ( | |
| 6 | + "crypto/hmac" | |
| 7 | + "crypto/sha256" | |
| 8 | + "encoding/hex" | |
| 9 | +) | |
| 10 | + | |
| 11 | +// SignSHA256 returns the X-Shithub-Signature-256 header value for the | |
| 12 | +// given body and per-webhook secret. The format mirrors GitHub's | |
| 13 | +// (`sha256=<hex>`) so existing receiver libraries verify cleanly. | |
| 14 | +func SignSHA256(secret, body []byte) string { | |
| 15 | + mac := hmac.New(sha256.New, secret) | |
| 16 | + mac.Write(body) | |
| 17 | + return "sha256=" + hex.EncodeToString(mac.Sum(nil)) | |
| 18 | +} | |
| 19 | + | |
| 20 | +// VerifySHA256 returns true when sig matches HMAC-SHA256(secret, body). | |
| 21 | +// Uses constant-time compare so the verifier doesn't leak timing info. | |
| 22 | +// Provided as the receiver-side helper that test code (and any future | |
| 23 | +// inbound webhook surface) can reuse. | |
| 24 | +func VerifySHA256(secret, body []byte, sig string) bool { | |
| 25 | + want := SignSHA256(secret, body) | |
| 26 | + if len(want) != len(sig) { | |
| 27 | + return false | |
| 28 | + } | |
| 29 | + return hmac.Equal([]byte(want), []byte(sig)) | |
| 30 | +} | |
internal/webhook/sign_test.goadded@@ -0,0 +1,41 @@ | ||
| 1 | +// SPDX-License-Identifier: AGPL-3.0-or-later | |
| 2 | + | |
| 3 | +package webhook | |
| 4 | + | |
| 5 | +import "testing" | |
| 6 | + | |
| 7 | +func TestSignAndVerifyRoundtrip(t *testing.T) { | |
| 8 | + secret := []byte("super-secret") | |
| 9 | + body := []byte(`{"hello":"world"}`) | |
| 10 | + | |
| 11 | + sig := SignSHA256(secret, body) | |
| 12 | + if !VerifySHA256(secret, body, sig) { | |
| 13 | + t.Fatalf("VerifySHA256 returned false on a freshly signed body") | |
| 14 | + } | |
| 15 | +} | |
| 16 | + | |
| 17 | +func TestVerifyRejectsTamperedBody(t *testing.T) { | |
| 18 | + secret := []byte("super-secret") | |
| 19 | + body := []byte(`{"hello":"world"}`) | |
| 20 | + sig := SignSHA256(secret, body) | |
| 21 | + | |
| 22 | + tampered := []byte(`{"hello":"WORLD"}`) | |
| 23 | + if VerifySHA256(secret, tampered, sig) { | |
| 24 | + t.Fatalf("VerifySHA256 accepted tampered body") | |
| 25 | + } | |
| 26 | +} | |
| 27 | + | |
| 28 | +func TestVerifyRejectsWrongSecret(t *testing.T) { | |
| 29 | + body := []byte(`{"x":1}`) | |
| 30 | + sig := SignSHA256([]byte("alice"), body) | |
| 31 | + if VerifySHA256([]byte("bob"), body, sig) { | |
| 32 | + t.Fatalf("VerifySHA256 accepted wrong secret") | |
| 33 | + } | |
| 34 | +} | |
| 35 | + | |
| 36 | +func TestSignaturePrefix(t *testing.T) { | |
| 37 | + sig := SignSHA256([]byte("k"), []byte("v")) | |
| 38 | + if got := sig[:7]; got != "sha256=" { | |
| 39 | + t.Fatalf("signature prefix = %q; want %q", got, "sha256=") | |
| 40 | + } | |
| 41 | +} | |