Added panic mode protections to make the server more secure

This commit is contained in:
2025-08-22 18:54:10 -04:00
parent 5067913c21
commit 5dfc710ae9
7 changed files with 430 additions and 247 deletions

View File

@@ -15,6 +15,7 @@ import (
"log"
"math/big"
"net/http"
"os"
"sort"
"strconv"
"strings"
@@ -55,6 +56,9 @@ type Server struct {
signingKey []byte
discord DiscordProvider
// rate limiter
rl *rateLimiter
// device-key challenge cache
chMu sync.Mutex
chal map[string]time.Time // nonce -> expires
@@ -70,7 +74,7 @@ type Server struct {
func New(store BlobStore, idx *index.Index, coarseTS, zeroTrust bool, providers AuthProviders, encRequired bool) *Server {
key, _ := hex.DecodeString(strings.TrimSpace(providers.SigningSecretHex))
return &Server{
s := &Server{
store: store,
idx: idx,
zeroTrust: zeroTrust,
@@ -83,6 +87,9 @@ func New(store BlobStore, idx *index.Index, coarseTS, zeroTrust bool, providers
replay: make(map[string]time.Time),
sseSub: make(map[chan string]struct{}),
}
// Default limiter: ~2 req/sec, burst 20, 10 min idle eviction
s.rl = newRateLimiter(2.0, 20, 10*time.Minute)
return s
}
func (s *Server) ListenHTTP(addr string) error {
@@ -108,19 +115,22 @@ func (s *Server) routes() *http.ServeMux {
mux.HandleFunc("/v1/auth/key/verify", s.handleAuthKeyVerify)
// Objects
mux.HandleFunc("/v1/object", s.handlePutObject) // PUT
mux.HandleFunc("/v1/object/", s.handleObjectByHash) // GET/DELETE
mux.HandleFunc("/v1/object", s.handlePutObject) // PUT (requires bearer+PoP)
mux.HandleFunc("/v1/object/", s.handleObjectByHash) // GET/DELETE (DELETE requires bearer+PoP)
// Index
mux.HandleFunc("/v1/index", s.handleIndexList)
mux.HandleFunc("/v1/index/stream", s.handleIndexStream)
// Admin
// Admin (requires admin device sub)
mux.HandleFunc("/v1/admin/reindex", s.handleAdminReindex)
// GDPR
mux.HandleFunc("/v1/gdpr/policy", s.handleGDPRPolicy)
// Session panic wipe (no auth; clears browser caches)
mux.HandleFunc("/v1/session/clear", s.handleSessionClear)
return mux
}
@@ -138,14 +148,42 @@ func (s *Server) cors(next http.Handler) http.Handler {
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("Strict-Transport-Security", "max-age=15552000; includeSubDomains; preload")
// Preflight is free (dont rate-limit OPTIONS)
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
// Rate limit (by device key CNF when available; else IP)
key := ""
if b := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer "); b != "" {
if ac, err := s.parseBearer(b); err == nil && ac.CNF != "" {
key = ac.CNF
}
}
if key == "" {
key = "ip:" + clientIP(r)
}
if !s.rl.allow(key) {
w.Header().Set("Retry-After", "5")
http.Error(w, "rate limited", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
func (s *Server) handleSessionClear(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Clear-Site-Data", `"cache","storage"`)
w.WriteHeader(http.StatusNoContent)
}
// ---------- Helpers ----------
func b64u(b []byte) string {
@@ -159,6 +197,21 @@ func b64ud(s string) ([]byte, error) {
}
func nowUTC() time.Time { return time.Now().UTC() }
// Admin allow-list: env GC_ADMIN_SUBS="sub1,sub2"
func (s *Server) isAdmin(ac authContext) bool {
raw := strings.TrimSpace(os.Getenv("GC_ADMIN_SUBS"))
if raw == "" || ac.CNF == "" {
return false
}
sub := thumbprintFromCNF(ac.CNF)
for _, x := range strings.Split(raw, ",") {
if sub == strings.TrimSpace(x) {
return true
}
}
return false
}
// ---------- Health ----------
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
@@ -202,7 +255,7 @@ type keyVerifyReq struct {
Nonce string `json:"nonce"`
Alg string `json:"alg"` // "p256"
Pub string `json:"pub"` // base64url raw (uncompressed point, 65 bytes)
Sig string `json:"sig"` // base64url DER(ECDSA)
Sig string `json:"sig"` // base64url DER(ECDSA) or raw r||s (64B)
}
func (s *Server) handleAuthKeyVerify(w http.ResponseWriter, r *http.Request) {
@@ -246,7 +299,7 @@ func (s *Server) handleAuthKeyVerify(w http.ResponseWriter, r *http.Request) {
writeErr(http.StatusBadRequest, "bad pub")
return
}
sigDER, err := b64ud(req.Sig)
sig, err := b64ud(req.Sig)
if err != nil {
writeErr(http.StatusBadRequest, "bad sig")
return
@@ -256,8 +309,8 @@ func (s *Server) handleAuthKeyVerify(w http.ResponseWriter, r *http.Request) {
x := new(big.Int).SetBytes(pubRaw[1:33])
y := new(big.Int).SetBytes(pubRaw[33:])
ek := &ecdsa.PublicKey{Curve: elliptic.P256(), X: x, Y: y}
if !ecdsaVerify(ek, msg, sigDER) {
http.Error(w, "verify failed", http.StatusUnauthorized)
if !ecdsaVerify(ek, msg, sig) {
writeErr(http.StatusUnauthorized, "verify failed")
return
}
// mint token
@@ -293,13 +346,13 @@ func ecdsaVerify(pub *ecdsa.PublicKey, msg []byte, sig []byte) bool {
return false
}
// Minimal DER parser that tolerates long-form lengths and leading 0x00 in INTEGERs.
// Minimal DER parser tolerant to long-form lengths and leading 0x00.
func parseECDSADER(der []byte) (*big.Int, *big.Int, bool) {
if len(der) < 8 || der[0] != 0x30 {
return nil, nil, false
}
i := 1
// Read SEQUENCE length (short or long form)
// sequence len
if i >= len(der) {
return nil, nil, false
}
@@ -318,12 +371,10 @@ func parseECDSADER(der []byte) (*big.Int, *big.Int, bool) {
seqLen = int(der[i])
i++
}
if i >= len(der) {
return nil, nil, false
}
_ = seqLen // not strictly needed
// INTEGER R
if der[i] != 0x02 {
if i >= len(der) || der[i] != 0x02 {
return nil, nil, false
}
i++
@@ -337,6 +388,7 @@ func parseECDSADER(der []byte) (*big.Int, *big.Int, bool) {
}
rb := der[i : i+rLen]
i += rLen
// INTEGER S
if i >= len(der) || der[i] != 0x02 {
return nil, nil, false
@@ -418,7 +470,6 @@ func (s *Server) gc2Mint(cnf string, exp time.Time) (string, error) {
}
hdr := map[string]string{"alg": "HS256", "typ": "GC2"}
iat := nowUTC().Unix()
// Fix: assign the sum before slicing to avoid "unaddressable value"
sum := sha256.Sum256([]byte(fmt.Sprintf("%d", time.Now().UnixNano())))
jti := b64u(sum[:16])
@@ -480,7 +531,8 @@ func (s *Server) verifyPoP(w http.ResponseWriter, r *http.Request, ac authContex
return false
}
now := nowUTC().Unix()
if ts < now-600 || ts > now+600 {
// Tight window: ±120s
if ts < now-120 || ts > now+120 {
http.Error(w, "ts window", http.StatusUnauthorized)
return false
}
@@ -496,7 +548,7 @@ func (s *Server) verifyPoP(w http.ResponseWriter, r *http.Request, ac authContex
http.Error(w, "replay", http.StatusUnauthorized)
return false
}
s.replay[proof] = nowUTC().Add(10 * time.Minute)
s.replay[proof] = nowUTC().Add(2 * time.Minute)
s.rpMu.Unlock()
// Verify signature over METHOD \n PATH \n TS \n SHA256(bodyHex)
@@ -513,7 +565,7 @@ func (s *Server) verifyPoP(w http.ResponseWriter, r *http.Request, ac authContex
http.Error(w, "bad key", http.StatusUnauthorized)
return false
}
sigDER, err := b64ud(proof)
sig, err := b64ud(proof)
if err != nil {
http.Error(w, "bad proof", http.StatusUnauthorized)
return false
@@ -521,7 +573,7 @@ func (s *Server) verifyPoP(w http.ResponseWriter, r *http.Request, ac authContex
x := new(big.Int).SetBytes(pubRaw[1:33])
y := new(big.Int).SetBytes(pubRaw[33:])
ek := &ecdsa.PublicKey{Curve: elliptic.P256(), X: x, Y: y}
if !ecdsaVerify(ek, msg, sigDER) {
if !ecdsaVerify(ek, msg, sig) {
http.Error(w, "pop verify", http.StatusUnauthorized)
return false
}
@@ -706,7 +758,6 @@ func (s *Server) sseBroadcastJSON(v any) {
select {
case ch <- string(b):
default:
// drop if slow
}
}
s.sseMu.Unlock()
@@ -720,7 +771,8 @@ func (s *Server) handleAdminReindex(w http.ResponseWriter, r *http.Request) {
return
}
ac, _ := s.parseBearer(strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer "))
if s.requirePOP && !s.verifyPoP(w, r, ac, nil) {
if !s.verifyPoP(w, r, ac, nil) || !s.isAdmin(ac) {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
var walked, indexed int64