Frontend Labs
⋯

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.ConstantTimeCompare is 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".


Generated from week6.md · 8 Oct 2026 16:36