package authn import ( "crypto/rand" "encoding/base64" "encoding/json" "net/http" "net/http/httptest" "os" "testing" "github.com/fernet/fernet-go" "github.com/vaughan0/go-ini" "sourcecraft.dev/bigbes/sr-ht-core/auth" "sourcecraft.dev/bigbes/sr-ht-core/crypto" "sourcecraft.dev/bigbes/sr-ht-ecore/login" ) // testConf is the synthesized instance config every test runs against: a fresh // random Fernet network-key, a random ed25519 webhook seed (crypto.InitCrypto // fatally requires one even though v1 emits no webhooks), the owner identity // the provenance builder reads, and our own section's origin. var testConf ini.File // rotatedKey stands in for a Fernet key we no longer hold — a cookie sealed // with it is indistinguishable from a forgery, which is exactly the point. var rotatedKey fernet.Key // TestMain builds that config in memory and runs crypto.InitCrypto once, so // crypto.Encrypt / DecryptWithoutExpiration share a keyset across the package. // No network, no Postgres, no files. func TestMain(m *testing.M) { var fk fernet.Key if err := fk.Generate(); err != nil { panic("generate fernet network key: " + err.Error()) } if err := rotatedKey.Generate(); err != nil { panic("generate rotated fernet key: " + err.Error()) } seed := make([]byte, 32) if _, err := rand.Read(seed); err != nil { panic("generate webhook seed: " + err.Error()) } testConf = ini.File{ "sr.ht": ini.Section{ "network-key": fk.Encode(), "owner-name": "bigbes", "owner-email": "bigbes@gmail.com", }, "webhooks": ini.Section{ "private-key": base64.StdEncoding.EncodeToString(seed), }, ConfigSection: ini.Section{ "origin": "https://spec.srht.bigb.es", }, } crypto.InitCrypto(testConf) os.Exit(m.Run()) } // sealCookie forges a valid unified-login cookie value carrying name, sealed // with the instance network key — byte for byte what meta.sr.ht would set. func sealCookie(t *testing.T, name string) string { t.Helper() payload, err := json.Marshal(auth.AuthCookie{Name: name}) if err != nil { t.Fatalf("marshal cookie claims: %v", err) } return string(crypto.Encrypt(payload)) } // sealCookieWithKey forges a cookie under an arbitrary Fernet key, used to // stand in for a rotated key or an attacker's own. func sealCookieWithKey(t *testing.T, key *fernet.Key, name string) string { t.Helper() payload, err := json.Marshal(auth.AuthCookie{Name: name}) if err != nil { t.Fatalf("marshal cookie claims: %v", err) } tok, err := fernet.EncryptAndSign(payload, key) if err != nil { t.Fatalf("seal cookie: %v", err) } return string(tok) } // tamper flips one character in the middle of a Fernet token, leaving it // well-formed base64 so that the HMAC check — not the decoder — is what // rejects it. func tamper(t *testing.T, token string) string { t.Helper() if len(token) < 8 { t.Fatalf("token too short to tamper with: %q", token) } b := []byte(token) i := len(b) / 2 if b[i] == 'A' { b[i] = 'B' } else { b[i] = 'A' } if string(b) == token { t.Fatal("tamper produced an identical token") } return string(b) } // testInstance is the provenance identity set built from testConf. func testInstance(t *testing.T) Instance { t.Helper() inst, err := InstanceFromConfig(testConf) if err != nil { t.Fatalf("InstanceFromConfig: %v", err) } return inst } // request builds a GET / carrying the given cookie value and headers. An empty // cookie value means no cookie at all. func request(cookie string, headers map[string]string) *http.Request { r := httptest.NewRequest(http.MethodGet, "/", nil) if cookie != "" { r.AddCookie(&http.Cookie{Name: login.CookieName, Value: cookie}) } for k, v := range headers { r.Header.Set(k, v) } return r }