Day 104 — Proof-of-Work & Invisible Challenges
| Learning objective | Issue and verify SHA256 PoW challenges; gate session cookies on completion |
| Key packages | crypto/sha256, crypto/rand, crypto/hmac, net/http, encoding/hex |
| Security context | PoW imposes computational cost on bots while allowing human users through (they solve it in JS) |
Key Concepts
PoW mechanics:
- Server issues
{challenge: random_hex, difficulty: 16, expires: unix_timestamp} - Client finds
noncesuch thatSHA256(challenge + nonce)has ≥ 16 leading zero bits - Expected attempts: 2^16 = 65,536 (takes ~65ms on modern CPU)
- Difficulty=20 → ~1 million attempts → 1 second — too slow for human-facing challenges
Session cookie issuance: After PoW solved, issue signed cookie. Subsequent requests verify cookie rather than re-challenging.
Exercise — PoW Challenge Endpoint + Session Cookie
package bot
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"net/http"
"strconv"
"strings"
"time"
)
type PowSystem struct {
secret []byte
difficulty int
cookieName string
expiry time.Duration
}
func NewPowSystem(secret []byte, difficulty int) *PowSystem {
return &PowSystem{
secret: secret,
difficulty: difficulty,
cookieName: "_bot_challenge",
expiry: 30 * time.Minute,
}
}
// ChallengeHandler issues a PoW challenge
func (ps *PowSystem) ChallengeHandler(w http.ResponseWriter, r *http.Request) {
raw := make([]byte, 16)
rand.Read(raw)
expires := time.Now().Add(5 * time.Minute).Unix()
// HMAC-sign the challenge + expiry
mac := hmac.New(sha256.New, ps.secret)
mac.Write(raw)
mac.Write([]byte(strconv.FormatInt(expires, 10)))
sig := hex.EncodeToString(mac.Sum(nil)[:8])
challengeHex := hex.EncodeToString(raw)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"challenge": challengeHex + "." + sig, // embed sig for verification
"difficulty": ps.difficulty,
"expires": expires,
})
}
// VerifyHandler validates a solved PoW and issues a session cookie
func (ps *PowSystem) VerifyHandler(w http.ResponseWriter, r *http.Request) {
var req struct {
Challenge string `json:"challenge"`
Nonce string `json:"nonce"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, `{"error":"invalid_request"}`, http.StatusBadRequest)
return
}
parts := strings.SplitN(req.Challenge, ".", 2)
if len(parts) != 2 {
http.Error(w, `{"error":"invalid_challenge"}`, http.StatusBadRequest)
return
}
challengeHex, storedSig := parts[0], parts[1]
challengeBytes, err := hex.DecodeString(challengeHex)
if err != nil {
http.Error(w, `{"error":"invalid_challenge"}`, http.StatusBadRequest)
return
}
// Verify HMAC (simplified — in production include expiry in MAC)
mac := hmac.New(sha256.New, ps.secret)
mac.Write(challengeBytes)
expectedSig := hex.EncodeToString(mac.Sum(nil)[:8])
if !hmac.Equal([]byte(storedSig), []byte(expectedSig)) {
http.Error(w, `{"error":"invalid_challenge"}`, http.StatusBadRequest)
return
}
// Verify nonce
nonce, err := hex.DecodeString(req.Nonce)
if err != nil {
http.Error(w, `{"error":"invalid_nonce"}`, http.StatusBadRequest)
return
}
h := sha256.New()
h.Write(challengeBytes)
h.Write(nonce)
hash := h.Sum(nil)
if leadingZeroBits(hash) < ps.difficulty {
http.Error(w, `{"error":"insufficient_work"}`, http.StatusBadRequest)
return
}
// Issue session cookie
sessionToken := ps.issueSessionToken()
http.SetCookie(w, &http.Cookie{
Name: ps.cookieName,
Value: sessionToken,
Path: "/",
Expires: time.Now().Add(ps.expiry),
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteStrictMode,
})
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "verified"})
}
func (ps *PowSystem) issueSessionToken() string {
raw := make([]byte, 16)
rand.Read(raw)
mac := hmac.New(sha256.New, ps.secret)
mac.Write(raw)
mac.Write([]byte(strconv.FormatInt(time.Now().Unix(), 10)))
return hex.EncodeToString(raw) + "." + hex.EncodeToString(mac.Sum(nil)[:8])
}
// HasValidSession checks if request has a valid PoW-issued session cookie
func (ps *PowSystem) HasValidSession(r *http.Request) bool {
cookie, err := r.Cookie(ps.cookieName)
if err != nil {
return false
}
parts := strings.SplitN(cookie.Value, ".", 2)
if len(parts) != 2 {
return false
}
// In production: verify HMAC and check expiry
// For brevity: just check format
_, err1 := hex.DecodeString(parts[0])
_, err2 := hex.DecodeString(parts[1])
return err1 == nil && err2 == nil
}