Day 42 — Hashing & HMAC
Learning Objectives
- Implement HMAC-SHA256 request signing — the pattern used by AWS SigV4 and webhook validation
- Understand timing attacks and why
subtle.ConstantTimeCompareis required - Build a webhook validation system (OWASP API Security — A02 Broken Authentication)
Key Concepts
// Hashing — one-way, no key
hash := sha256.Sum256([]byte("data"))
// HMAC — keyed hash; proves the sender knows the shared secret
mac := hmac.New(sha256.New, []byte("secret-key"))
mac.Write([]byte("message"))
signature := mac.Sum(nil)
// TIMING ATTACK: bytes.Equal reveals how many bytes match
// Attacker can guess HMAC byte-by-byte by measuring response time
if bytes.Equal(computed, received) { } // WRONG
// SAFE: constant-time compare — always takes the same time
if subtle.ConstantTimeCompare(computed, received) == 1 { } // RIGHT
Exercise: Webhook Signing System
package main
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"fmt"
"net/http"
"strconv"
"time"
)
const webhookSecret = "your-webhook-secret-key-here"
// SignWebhook adds HMAC-SHA256 signature to outgoing webhook
// Signature = HMAC-SHA256(key, "timestamp.body")
func SignWebhook(body []byte, secret string) (signature, timestamp string) {
ts := strconv.FormatInt(time.Now().Unix(), 10)
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(ts + "."))
mac.Write(body)
return "sha256=" + hex.EncodeToString(mac.Sum(nil)), ts
}
// VerifyWebhook validates an incoming webhook signature
// Returns error if invalid or replayed (timestamp > 5min old)
func VerifyWebhook(body []byte, signature, timestamp, secret string) error {
// Check timestamp freshness — prevent replay attacks
ts, err := strconv.ParseInt(timestamp, 10, 64)
if err != nil {
return fmt.Errorf("invalid timestamp")
}
age := time.Since(time.Unix(ts, 0))
if age > 5*time.Minute || age < -time.Minute {
return fmt.Errorf("webhook timestamp too old or too far in future: age=%v", age)
}
// Compute expected signature
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(timestamp + "."))
mac.Write(body)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
// Constant-time compare — prevents timing attacks
// Without this, an attacker can measure response time to guess signature byte-by-byte
if subtle.ConstantTimeCompare([]byte(expected), []byte(signature)) != 1 {
return fmt.Errorf("signature mismatch")
}
return nil
}
// WebhookSender sends a signed webhook
func WebhookSender(targetURL string, payload []byte) error {
sig, ts := SignWebhook(payload, webhookSecret)
req, _ := http.NewRequest("POST", targetURL, bytes.NewReader(payload))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Webhook-Signature", sig)
req.Header.Set("X-Webhook-Timestamp", ts)
resp, err := http.DefaultClient.Do(req)
if err != nil { return err }
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
return fmt.Errorf("webhook rejected: %d", resp.StatusCode)
}
return nil
}
// WebhookReceiver validates incoming webhooks
func WebhookReceiver(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20)) // 1MB limit
if err != nil {
http.Error(w, "read error", 500)
return
}
sig := r.Header.Get("X-Webhook-Signature")
ts := r.Header.Get("X-Webhook-Timestamp")
if err := VerifyWebhook(body, sig, ts, webhookSecret); err != nil {
// Log the attempt — could be an attack
fmt.Printf("WEBHOOK INVALID: %v from %s\n", err, r.RemoteAddr)
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
fmt.Printf("WEBHOOK VALID: %s\n", body)
w.WriteHeader(http.StatusNoContent)
}
func main() {
http.HandleFunc("/webhook", WebhookReceiver)
fmt.Println("Webhook receiver on :8080/webhook")
// Test: go to Day 42 exercise to send test webhooks
http.ListenAndServe(":8080", nil)
}
Real-World Context
GitHub, Stripe, Shopify, and Twilio all use HMAC-SHA256 webhook signing with this exact pattern. When your team integrates with any webhook-based service, verify the signature. Without it, an attacker can forge events: "order shipped", "payment received", "user admin escalated".