diff --git a/prover/server/bench.sh b/prover/server/scripts/bench.sh similarity index 100% rename from prover/server/bench.sh rename to prover/server/scripts/bench.sh diff --git a/prover/server/scripts/test_auth.sh b/prover/server/scripts/test_auth.sh new file mode 100755 index 0000000000..20c6217e9d --- /dev/null +++ b/prover/server/scripts/test_auth.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +# Test script for API key authentication on prover server +# This script demonstrates how to test the API key functionality + +echo "Testing Prover Server API Key Authentication" +echo "=============================================" + +# Test variables +SERVER_URL="http://localhost:3001" +API_KEY="test-api-key-12345" + +echo "" +echo "1. Testing /health endpoint (should work without API key):" +curl -s -o /dev/null -w "%{http_code}" $SERVER_URL/health +echo "" + +echo "" +echo "2. Testing /prove endpoint without API key (should return 401):" +curl -s -o /dev/null -w "%{http_code}" -X POST $SERVER_URL/prove -H "Content-Type: application/json" -d '{"circuit_type": "inclusion"}' +echo "" + +echo "" +echo "3. Testing /prove endpoint with X-API-Key header (should work if server has API key):" +curl -s -o /dev/null -w "%{http_code}" -X POST $SERVER_URL/prove -H "Content-Type: application/json" -H "X-API-Key: $API_KEY" -d '{"circuit_type": "inclusion"}' +echo "" + +echo "" +echo "4. Testing /prove endpoint with Authorization Bearer header (should work if server has API key):" +curl -s -o /dev/null -w "%{http_code}" -X POST $SERVER_URL/prove -H "Content-Type: application/json" -H "Authorization: Bearer $API_KEY" -d '{"circuit_type": "inclusion"}' +echo "" + +echo "" +echo "To run this test:" +echo "1. Set PROVER_API_KEY environment variable: export PROVER_API_KEY=test-api-key-12345" +echo "2. Start the prover server +echo "3. Run this script: bash test_auth.sh" +echo "" +echo "Expected results:" +echo "- /health: 200 (always accessible)" +echo "- /prove without key: 401 (if API key is set)" +echo "- /prove with key: 400 or other (depends on valid circuit data)" diff --git a/prover/server/server/auth.go b/prover/server/server/auth.go new file mode 100644 index 0000000000..4e23b3c428 --- /dev/null +++ b/prover/server/server/auth.go @@ -0,0 +1,101 @@ +package server + +import ( + "crypto/subtle" + "light/light-prover/logging" + "net/http" + "os" + "strings" +) + +type authMiddleware struct { + next http.Handler + apiKey string +} + +func NewAPIKeyMiddleware(apiKey string) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return &authMiddleware{ + next: next, + apiKey: apiKey, + } + } +} + +func (m *authMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if !m.isAuthenticated(r) { + logging.Logger().Warn(). + Str("remote_addr", r.RemoteAddr). + Str("path", r.URL.Path). + Str("method", r.Method). + Msg("Unauthorized API request - missing or invalid API key") + + unauthorizedError := &Error{ + StatusCode: http.StatusUnauthorized, + Code: "unauthorized", + Message: "Invalid or missing API key. Please provide a valid API key in the Authorization header as 'Bearer ' or in the X-API-Key header.", + } + unauthorizedError.send(w) + return + } + + m.next.ServeHTTP(w, r) +} + +func (m *authMiddleware) isAuthenticated(r *http.Request) bool { + if m.apiKey == "" { + return true + } + + providedKey := m.extractAPIKey(r) + if providedKey == "" { + return false + } + + return subtle.ConstantTimeCompare([]byte(m.apiKey), []byte(providedKey)) == 1 +} + +func (m *authMiddleware) extractAPIKey(r *http.Request) string { + if apiKey := r.Header.Get("X-API-Key"); apiKey != "" { + return apiKey + } + + if authHeader := r.Header.Get("Authorization"); authHeader != "" { + if strings.HasPrefix(authHeader, "Bearer ") { + return strings.TrimPrefix(authHeader, "Bearer ") + } + } + + return "" +} + +func getAPIKeyFromEnv() string { + return os.Getenv("PROVER_API_KEY") +} + +func requiresAuthentication(path string) bool { + publicPaths := []string{ + "/health", + } + + for _, publicPath := range publicPaths { + if path == publicPath { + return false + } + } + + return true +} + +func conditionalAuthMiddleware(apiKey string) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if requiresAuthentication(r.URL.Path) { + authHandler := NewAPIKeyMiddleware(apiKey)(next) + authHandler.ServeHTTP(w, r) + } else { + next.ServeHTTP(w, r) + } + }) + } +} \ No newline at end of file diff --git a/prover/server/server/server.go b/prover/server/server/server.go index b12ebdc6dd..ed6d43dd56 100644 --- a/prover/server/server/server.go +++ b/prover/server/server/server.go @@ -453,6 +453,12 @@ func RunWithQueue(config *Config, redisQueue *RedisQueue, circuits []string, run } func RunEnhanced(config *EnhancedConfig, redisQueue *RedisQueue, circuits []string, runMode prover.RunMode, provingSystemsV1 []*prover.ProvingSystemV1, provingSystemsV2 []*prover.ProvingSystemV2) RunningJob { + apiKey := getAPIKeyFromEnv() + if apiKey != "" { + logging.Logger().Info().Msg("API key authentication enabled for prover server") + } else { + logging.Logger().Warn().Msg("No API key configured - server will accept all requests. Set PROVER_API_KEY environment variable to enable authentication.") + } metricsMux := http.NewServeMux() metricsServer := &http.Server{Addr: config.MetricsAddress, Handler: metricsMux} metricsJob := spawnServerJob(metricsServer, "metrics server") @@ -531,6 +537,7 @@ func RunEnhanced(config *EnhancedConfig, redisQueue *RedisQueue, circuits []stri "X-Requested-With", "Content-Type", "Authorization", + "X-API-Key", "X-Async", "X-Sync", }), @@ -538,7 +545,8 @@ func RunEnhanced(config *EnhancedConfig, redisQueue *RedisQueue, circuits []stri handlers.AllowedMethods([]string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}), ) - proverServer := &http.Server{Addr: config.ProverAddress, Handler: corsHandler(proverMux)} + authHandler := conditionalAuthMiddleware(apiKey) + proverServer := &http.Server{Addr: config.ProverAddress, Handler: corsHandler(authHandler(proverMux))} proverJob := spawnServerJob(proverServer, "prover server") if redisQueue != nil {