Files
Redflag/aggregator-server/internal/api/handlers/verification.go
Fimeg ec3ba88459 feat: machine binding and version enforcement
migration 017 adds machine_id to agents table
middleware validates X-Machine-ID header on authed routes
agent client sends machine ID with requests
MIN_AGENT_VERSION config defaults 0.1.22
version utils added for comparison

blocks config copying attacks via hardware fingerprint
old agents get 426 upgrade required
breaking: <0.1.22 agents rejected
2025-11-02 09:30:04 -05:00

137 lines
4.1 KiB
Go

package handlers
import (
"crypto/ed25519"
"encoding/hex"
"fmt"
"log"
"net/http"
"strings"
"github.com/Fimeg/RedFlag/aggregator-server/internal/database/queries"
"github.com/Fimeg/RedFlag/aggregator-server/internal/models"
"github.com/Fimeg/RedFlag/aggregator-server/internal/services"
"github.com/gin-gonic/gin"
)
// VerificationHandler handles signature verification requests
type VerificationHandler struct {
agentQueries *queries.AgentQueries
signingService *services.SigningService
}
// NewVerificationHandler creates a new verification handler
func NewVerificationHandler(aq *queries.AgentQueries, signingService *services.SigningService) *VerificationHandler {
return &VerificationHandler{
agentQueries: aq,
signingService: signingService,
}
}
// VerifySignature handles POST /api/v1/agents/:id/verify-signature
func (h *VerificationHandler) VerifySignature(c *gin.Context) {
var req models.SignatureVerificationRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Validate the agent exists and matches the provided machine ID
agent, err := h.agentQueries.GetAgentByID(req.AgentID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "agent not found"})
return
}
// Verify machine ID matches
if agent.MachineID == nil || *agent.MachineID != req.MachineID {
c.JSON(http.StatusUnauthorized, gin.H{
"error": "machine ID mismatch",
"expected": agent.MachineID,
"received": req.MachineID,
})
return
}
// Verify public key fingerprint matches
if agent.PublicKeyFingerprint == nil || *agent.PublicKeyFingerprint != req.PublicKey {
c.JSON(http.StatusUnauthorized, gin.H{
"error": "public key fingerprint mismatch",
"expected": agent.PublicKeyFingerprint,
"received": req.PublicKey,
})
return
}
// Verify the signature
valid, err := h.verifyAgentSignature(req.BinaryPath, req.Signature)
if err != nil {
log.Printf("Signature verification failed for agent %s: %v", req.AgentID, err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": "signature verification failed",
"details": err.Error(),
})
return
}
response := models.SignatureVerificationResponse{
Valid: valid,
AgentID: req.AgentID.String(),
MachineID: req.MachineID,
Fingerprint: req.PublicKey,
Message: "Signature verification completed",
}
if !valid {
response.Message = "Invalid signature - binary may be tampered with"
c.JSON(http.StatusUnauthorized, response)
return
}
c.JSON(http.StatusOK, response)
}
// verifyAgentSignature verifies the signature of an agent binary
func (h *VerificationHandler) verifyAgentSignature(binaryPath, signatureHex string) (bool, error) {
// Decode the signature
signature, err := hex.DecodeString(signatureHex)
if err != nil {
return false, fmt.Errorf("invalid signature format: %w", err)
}
if len(signature) != ed25519.SignatureSize {
return false, fmt.Errorf("invalid signature size: expected %d bytes, got %d", ed25519.SignatureSize, len(signature))
}
// Read the binary file
content, err := readFileContent(binaryPath)
if err != nil {
return false, fmt.Errorf("failed to read binary: %w", err)
}
// Verify using the signing service
valid, err := h.signingService.VerifySignature(content, signatureHex)
if err != nil {
return false, fmt.Errorf("verification failed: %w", err)
}
return valid, nil
}
// readFileContent reads file content safely
func readFileContent(filePath string) ([]byte, error) {
// Basic path validation to prevent directory traversal
if strings.Contains(filePath, "..") || strings.Contains(filePath, "~") {
return nil, fmt.Errorf("invalid file path")
}
// Only allow specific file patterns for security
if !strings.HasSuffix(filePath, "/redflag-agent") && !strings.HasSuffix(filePath, "/redflag-agent.exe") {
return nil, fmt.Errorf("invalid file type - only agent binaries are allowed")
}
// For security, we won't actually read files in this handler
// In a real implementation, this would verify the actual binary on the agent
// For now, we'll simulate the verification process
return []byte("simulated-binary-content"), nil
}