tenseleyflow/shithub / 706426a

Browse files

S33: HMAC-SHA256 sign + verify

Authored by espadonne
SHA
706426a394c97b6e5da10cb5d687ea1d22bb432b
Parents
4a9b2e1
Tree
df5c738

2 changed files

StatusFile+-
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
+}