Files
metacrypt/internal/webserver/csrf_test.go
Kyle Isom 64d921827e Add MEK rotation, per-engine DEKs, and v2 ciphertext format (audit #6, #22)
Implement a two-level key hierarchy: the MEK now wraps per-engine DEKs
stored in a new barrier_keys table, rather than encrypting all barrier
entries directly. A v2 ciphertext format (0x02) embeds the key ID so the
barrier can resolve which DEK to use on decryption. v1 ciphertext remains
supported for backward compatibility.

Key changes:
- crypto: EncryptV2/DecryptV2/ExtractKeyID for v2 ciphertext with key IDs
- barrier: key registry (CreateKey, RotateKey, ListKeys, MigrateToV2, ReWrapKeys)
- seal: RotateMEK re-wraps DEKs without re-encrypting data
- engine: Mount auto-creates per-engine DEK
- REST + gRPC: barrier/keys, barrier/rotate-mek, barrier/rotate-key, barrier/migrate
- proto: BarrierService (v1 + v2) with ListKeys, RotateMEK, RotateKey, Migrate
- db: migration v2 adds barrier_keys table

Also includes: security audit report, CSRF protection, engine design specs
(sshca, transit, user), path-bound AAD migration tool, policy engine
enhancements, and ARCHITECTURE.md updates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 18:27:44 -07:00

106 lines
3.0 KiB
Go

package webserver
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestCSRFTokenGenerateAndValidate(t *testing.T) {
c := newCSRFProtect()
token, err := c.generateToken()
if err != nil {
t.Fatalf("generateToken: %v", err)
}
if !c.validToken(token) {
t.Fatal("valid token rejected")
}
}
func TestCSRFTokenInvalidFormats(t *testing.T) {
c := newCSRFProtect()
for _, bad := range []string{"", "nodot", "a.b.c", "abc.def"} {
if c.validToken(bad) {
t.Errorf("should reject %q", bad)
}
}
}
func TestCSRFTokenCrossSecret(t *testing.T) {
c1 := newCSRFProtect()
c2 := newCSRFProtect()
token, _ := c1.generateToken()
if c2.validToken(token) {
t.Fatal("token from different secret should be rejected")
}
}
func TestCSRFMiddlewareAllowsGET(t *testing.T) {
c := newCSRFProtect()
handler := c.middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
req := httptest.NewRequest(http.MethodGet, "/", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("GET should pass through, got %d", w.Code)
}
}
func TestCSRFMiddlewareBlocksPOSTWithoutToken(t *testing.T) {
c := newCSRFProtect()
handler := c.middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("foo=bar"))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
if w.Code != http.StatusForbidden {
t.Fatalf("POST without CSRF token should be forbidden, got %d", w.Code)
}
}
func TestCSRFMiddlewareAllowsPOSTWithValidToken(t *testing.T) {
c := newCSRFProtect()
token, _ := c.generateToken()
handler := c.middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
body := csrfFieldName + "=" + token
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.AddCookie(&http.Cookie{Name: csrfCookieName, Value: token})
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("POST with valid CSRF token should pass, got %d", w.Code)
}
}
func TestCSRFMiddlewareRejectsMismatch(t *testing.T) {
c := newCSRFProtect()
token1, _ := c.generateToken()
token2, _ := c.generateToken()
handler := c.middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
body := csrfFieldName + "=" + token1
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.AddCookie(&http.Cookie{Name: csrfCookieName, Value: token2})
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
if w.Code != http.StatusForbidden {
t.Fatalf("POST with mismatched tokens should be forbidden, got %d", w.Code)
}
}