Complete RedFlag codebase with two major security audit implementations.
== A-1: Ed25519 Key Rotation Support ==
Server:
- SignCommand sets SignedAt timestamp and KeyID on every signature
- signing_keys database table (migration 020) for multi-key rotation
- InitializePrimaryKey registers active key at startup
- /api/v1/public-keys endpoint for rotation-aware agents
- SigningKeyQueries for key lifecycle management
Agent:
- Key-ID-aware verification via CheckKeyRotation
- FetchAndCacheAllActiveKeys for rotation pre-caching
- Cache metadata with TTL and staleness fallback
- SecurityLogger events for key rotation and command signing
== A-2: Replay Attack Fixes (F-1 through F-7) ==
F-5 CRITICAL - RetryCommand now signs via signAndCreateCommand
F-1 HIGH - v3 format: "{agent_id}:{cmd_id}:{type}:{hash}:{ts}"
F-7 HIGH - Migration 026: expires_at column with partial index
F-6 HIGH - GetPendingCommands/GetStuckCommands filter by expires_at
F-2 HIGH - Agent-side executedIDs dedup map with cleanup
F-4 HIGH - commandMaxAge reduced from 24h to 4h
F-3 CRITICAL - Old-format commands rejected after 48h via CreatedAt
Verification fixes: migration idempotency (ETHOS #4), log format
compliance (ETHOS #1), stale comments updated.
All 24 tests passing. Docker --no-cache build verified.
See docs/ for full audit reports and deviation log (DEV-001 to DEV-019).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
125 lines
3.4 KiB
Go
125 lines
3.4 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
|
|
"github.com/Fimeg/RedFlag/aggregator-server/internal/database/queries"
|
|
"github.com/Fimeg/RedFlag/aggregator-server/internal/services"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// SystemHandler handles system-level operations
|
|
type SystemHandler struct {
|
|
signingService *services.SigningService
|
|
signingKeyQueries *queries.SigningKeyQueries
|
|
}
|
|
|
|
// NewSystemHandler creates a new system handler
|
|
func NewSystemHandler(ss *services.SigningService, skq *queries.SigningKeyQueries) *SystemHandler {
|
|
return &SystemHandler{
|
|
signingService: ss,
|
|
signingKeyQueries: skq,
|
|
}
|
|
}
|
|
|
|
// GetPublicKey returns the server's Ed25519 public key for signature verification.
|
|
// This allows agents to fetch the public key at runtime instead of embedding it at build time.
|
|
func (h *SystemHandler) GetPublicKey(c *gin.Context) {
|
|
if h.signingService == nil || !h.signingService.IsEnabled() {
|
|
c.JSON(http.StatusServiceUnavailable, gin.H{
|
|
"error": "signing service not configured",
|
|
"hint": "Set REDFLAG_SIGNING_PRIVATE_KEY environment variable",
|
|
})
|
|
return
|
|
}
|
|
|
|
pubKeyHex := h.signingService.GetPublicKey()
|
|
fingerprint := h.signingService.GetPublicKeyFingerprint()
|
|
keyID := h.signingService.GetCurrentKeyID()
|
|
|
|
// Try to get version from DB; fall back to 1 if unavailable
|
|
version := 1
|
|
if h.signingKeyQueries != nil {
|
|
ctx := context.Background()
|
|
if primaryKey, err := h.signingKeyQueries.GetPrimarySigningKey(ctx); err == nil {
|
|
version = primaryKey.Version
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"public_key": pubKeyHex,
|
|
"fingerprint": fingerprint,
|
|
"algorithm": "ed25519",
|
|
"key_size": 32,
|
|
"key_id": keyID,
|
|
"version": version,
|
|
})
|
|
}
|
|
|
|
// GetActivePublicKeys returns all currently active public keys for key-rotation-aware agents.
|
|
// This is a rate-limited public endpoint — no authentication required.
|
|
func (h *SystemHandler) GetActivePublicKeys(c *gin.Context) {
|
|
if h.signingService == nil || !h.signingService.IsEnabled() {
|
|
c.JSON(http.StatusServiceUnavailable, gin.H{
|
|
"error": "signing service not configured",
|
|
})
|
|
return
|
|
}
|
|
|
|
ctx := c.Request.Context()
|
|
activeKeys, err := h.signingService.GetAllActivePublicKeys(ctx)
|
|
|
|
// Build response — always return at least the current key
|
|
type keyEntry struct {
|
|
KeyID string `json:"key_id"`
|
|
PublicKey string `json:"public_key"`
|
|
IsPrimary bool `json:"is_primary"`
|
|
Version int `json:"version"`
|
|
Algorithm string `json:"algorithm"`
|
|
}
|
|
|
|
if err != nil || len(activeKeys) == 0 {
|
|
// Fall back to single-entry response with current key
|
|
c.JSON(http.StatusOK, []keyEntry{
|
|
{
|
|
KeyID: h.signingService.GetCurrentKeyID(),
|
|
PublicKey: h.signingService.GetPublicKeyHex(),
|
|
IsPrimary: true,
|
|
Version: 1,
|
|
Algorithm: "ed25519",
|
|
},
|
|
})
|
|
return
|
|
}
|
|
|
|
entries := make([]keyEntry, 0, len(activeKeys))
|
|
for _, k := range activeKeys {
|
|
entries = append(entries, keyEntry{
|
|
KeyID: k.KeyID,
|
|
PublicKey: k.PublicKey,
|
|
IsPrimary: k.IsPrimary,
|
|
Version: k.Version,
|
|
Algorithm: k.Algorithm,
|
|
})
|
|
}
|
|
|
|
c.JSON(http.StatusOK, entries)
|
|
}
|
|
|
|
// GetSystemInfo returns general system information
|
|
func (h *SystemHandler) GetSystemInfo(c *gin.Context) {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"version": "v0.1.21",
|
|
"name": "RedFlag Aggregator",
|
|
"description": "Self-hosted update management platform",
|
|
"features": []string{
|
|
"agent_management",
|
|
"update_tracking",
|
|
"command_execution",
|
|
"ed25519_signing",
|
|
"key_rotation",
|
|
},
|
|
})
|
|
}
|