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
This commit is contained in:
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/Fimeg/RedFlag/aggregator-agent/internal/circuitbreaker"
|
||||
"github.com/Fimeg/RedFlag/aggregator-agent/internal/client"
|
||||
"github.com/Fimeg/RedFlag/aggregator-agent/internal/config"
|
||||
"github.com/Fimeg/RedFlag/aggregator-agent/internal/crypto"
|
||||
"github.com/Fimeg/RedFlag/aggregator-agent/internal/display"
|
||||
"github.com/Fimeg/RedFlag/aggregator-agent/internal/installer"
|
||||
"github.com/Fimeg/RedFlag/aggregator-agent/internal/orchestrator"
|
||||
@@ -348,13 +349,28 @@ func registerAgent(cfg *config.Config, serverURL string) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Get machine ID for binding
|
||||
machineID, err := system.GetMachineID()
|
||||
if err != nil {
|
||||
log.Printf("Warning: Failed to get machine ID: %v", err)
|
||||
machineID = "unknown-" + sysInfo.Hostname
|
||||
}
|
||||
|
||||
// Get embedded public key fingerprint
|
||||
publicKeyFingerprint := system.GetPublicKeyFingerprint()
|
||||
if publicKeyFingerprint == "" {
|
||||
log.Printf("Warning: No embedded public key fingerprint found")
|
||||
}
|
||||
|
||||
req := client.RegisterRequest{
|
||||
Hostname: sysInfo.Hostname,
|
||||
OSType: sysInfo.OSType,
|
||||
OSVersion: sysInfo.OSVersion,
|
||||
OSArchitecture: sysInfo.OSArchitecture,
|
||||
AgentVersion: sysInfo.AgentVersion,
|
||||
Metadata: metadata,
|
||||
Hostname: sysInfo.Hostname,
|
||||
OSType: sysInfo.OSType,
|
||||
OSVersion: sysInfo.OSVersion,
|
||||
OSArchitecture: sysInfo.OSArchitecture,
|
||||
AgentVersion: sysInfo.AgentVersion,
|
||||
MachineID: machineID,
|
||||
PublicKeyFingerprint: publicKeyFingerprint,
|
||||
Metadata: metadata,
|
||||
}
|
||||
|
||||
resp, err := apiClient.Register(req)
|
||||
@@ -376,7 +392,27 @@ func registerAgent(cfg *config.Config, serverURL string) error {
|
||||
}
|
||||
|
||||
// Save configuration
|
||||
return cfg.Save(getConfigPath())
|
||||
if err := cfg.Save(getConfigPath()); err != nil {
|
||||
return fmt.Errorf("failed to save config: %w", err)
|
||||
}
|
||||
|
||||
// Fetch and cache server public key for signature verification
|
||||
log.Println("Fetching server public key for update signature verification...")
|
||||
if err := fetchAndCachePublicKey(cfg.ServerURL); err != nil {
|
||||
log.Printf("Warning: Failed to fetch server public key: %v", err)
|
||||
log.Printf("Agent will not be able to verify update signatures")
|
||||
// Don't fail registration - key can be fetched later
|
||||
} else {
|
||||
log.Println("✓ Server public key cached successfully")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// fetchAndCachePublicKey fetches the server's Ed25519 public key and caches it locally
|
||||
func fetchAndCachePublicKey(serverURL string) error {
|
||||
_, err := crypto.FetchAndCacheServerPublicKey(serverURL)
|
||||
return err
|
||||
}
|
||||
|
||||
// renewTokenIfNeeded handles 401 errors by renewing the agent token using refresh token
|
||||
@@ -694,6 +730,12 @@ func runAgent(cfg *config.Config) error {
|
||||
if err := handleReboot(apiClient, cfg, ackTracker, cmd.ID, cmd.Params); err != nil {
|
||||
log.Printf("[Reboot] Error processing reboot command: %v\n", err)
|
||||
}
|
||||
|
||||
case "update_agent":
|
||||
if err := handleUpdateAgent(apiClient, cfg, ackTracker, cmd.Params, cmd.ID); err != nil {
|
||||
log.Printf("[Update] Error processing agent update command: %v\n", err)
|
||||
}
|
||||
|
||||
default:
|
||||
log.Printf("Unknown command type: %s - reporting as invalid command\n", cmd.Type)
|
||||
// Report invalid command back to server
|
||||
|
||||
@@ -2,8 +2,18 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/ed25519"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/Fimeg/RedFlag/aggregator-agent/internal/acknowledgment"
|
||||
@@ -39,6 +49,10 @@ func handleScanUpdatesV2(apiClient *client.Client, cfg *config.Config, ackTracke
|
||||
Stderr: stderr,
|
||||
ExitCode: exitCode,
|
||||
DurationSeconds: int(duration.Seconds()),
|
||||
Metadata: map[string]string{
|
||||
"subsystem_label": "Package Updates",
|
||||
"subsystem": "updates",
|
||||
},
|
||||
}
|
||||
|
||||
// Report the scan log
|
||||
@@ -96,6 +110,10 @@ func handleScanStorage(apiClient *client.Client, cfg *config.Config, ackTracker
|
||||
Stderr: stderr,
|
||||
ExitCode: exitCode,
|
||||
DurationSeconds: int(duration.Seconds()),
|
||||
Metadata: map[string]string{
|
||||
"subsystem_label": "Disk Usage",
|
||||
"subsystem": "storage",
|
||||
},
|
||||
}
|
||||
|
||||
// Report the scan log
|
||||
@@ -150,6 +168,10 @@ func handleScanSystem(apiClient *client.Client, cfg *config.Config, ackTracker *
|
||||
Stderr: stderr,
|
||||
ExitCode: exitCode,
|
||||
DurationSeconds: int(duration.Seconds()),
|
||||
Metadata: map[string]string{
|
||||
"subsystem_label": "System Metrics",
|
||||
"subsystem": "system",
|
||||
},
|
||||
}
|
||||
|
||||
// Report the scan log
|
||||
@@ -204,6 +226,10 @@ func handleScanDocker(apiClient *client.Client, cfg *config.Config, ackTracker *
|
||||
Stderr: stderr,
|
||||
ExitCode: exitCode,
|
||||
DurationSeconds: int(duration.Seconds()),
|
||||
Metadata: map[string]string{
|
||||
"subsystem_label": "Docker Images",
|
||||
"subsystem": "docker",
|
||||
},
|
||||
}
|
||||
|
||||
// Report the scan log
|
||||
@@ -230,3 +256,550 @@ func handleScanDocker(apiClient *client.Client, cfg *config.Config, ackTracker *
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleUpdateAgent handles agent update commands with signature verification
|
||||
func handleUpdateAgent(apiClient *client.Client, cfg *config.Config, ackTracker *acknowledgment.Tracker, params map[string]interface{}, commandID string) error {
|
||||
log.Println("Processing agent update command...")
|
||||
|
||||
// Extract parameters
|
||||
version, ok := params["version"].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("missing version parameter")
|
||||
}
|
||||
|
||||
platform, ok := params["platform"].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("missing platform parameter")
|
||||
}
|
||||
|
||||
downloadURL, ok := params["download_url"].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("missing download_url parameter")
|
||||
}
|
||||
|
||||
signature, ok := params["signature"].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("missing signature parameter")
|
||||
}
|
||||
|
||||
checksum, ok := params["checksum"].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("missing checksum parameter")
|
||||
}
|
||||
|
||||
// Extract nonce parameters for replay protection
|
||||
nonceUUIDStr, ok := params["nonce_uuid"].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("missing nonce_uuid parameter")
|
||||
}
|
||||
|
||||
nonceTimestampStr, ok := params["nonce_timestamp"].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("missing nonce_timestamp parameter")
|
||||
}
|
||||
|
||||
nonceSignature, ok := params["nonce_signature"].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("missing nonce_signature parameter")
|
||||
}
|
||||
|
||||
log.Printf("Updating agent to version %s (%s)", version, platform)
|
||||
|
||||
// Validate nonce for replay protection
|
||||
log.Printf("[tunturi_ed25519] Validating nonce...")
|
||||
if err := validateNonce(nonceUUIDStr, nonceTimestampStr, nonceSignature); err != nil {
|
||||
return fmt.Errorf("[tunturi_ed25519] nonce validation failed: %w", err)
|
||||
}
|
||||
log.Printf("[tunturi_ed25519] ✓ Nonce validated")
|
||||
|
||||
// Record start time for duration calculation
|
||||
updateStartTime := time.Now()
|
||||
|
||||
// Report the update command as started
|
||||
logReport := client.LogReport{
|
||||
CommandID: commandID,
|
||||
Action: "update_agent",
|
||||
Result: "started",
|
||||
Stdout: fmt.Sprintf("Starting agent update to version %s\n", version),
|
||||
Stderr: "",
|
||||
ExitCode: 0,
|
||||
DurationSeconds: 0,
|
||||
Metadata: map[string]string{
|
||||
"subsystem_label": "Agent Update",
|
||||
"subsystem": "agent",
|
||||
"target_version": version,
|
||||
},
|
||||
}
|
||||
|
||||
if err := reportLogWithAck(apiClient, cfg, ackTracker, logReport); err != nil {
|
||||
log.Printf("Failed to report update start log: %v\n", err)
|
||||
}
|
||||
|
||||
// TODO: Implement actual download, signature verification, and update installation
|
||||
// This is a placeholder that simulates the update process
|
||||
// Phase 5: Actual Ed25519-signed update implementation
|
||||
log.Printf("Starting secure update process for version %s", version)
|
||||
log.Printf("Download URL: %s", downloadURL)
|
||||
log.Printf("Signature: %s...", signature[:16]) // Log first 16 chars of signature
|
||||
log.Printf("Expected checksum: %s", checksum)
|
||||
|
||||
// Step 1: Download the update package
|
||||
log.Printf("Step 1: Downloading update package...")
|
||||
tempBinaryPath, err := downloadUpdatePackage(downloadURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to download update package: %w", err)
|
||||
}
|
||||
defer os.Remove(tempBinaryPath) // Cleanup on exit
|
||||
|
||||
// Step 2: Verify checksum
|
||||
log.Printf("Step 2: Verifying checksum...")
|
||||
actualChecksum, err := computeSHA256(tempBinaryPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to compute checksum: %w", err)
|
||||
}
|
||||
|
||||
if actualChecksum != checksum {
|
||||
return fmt.Errorf("checksum mismatch: expected %s, got %s", checksum, actualChecksum)
|
||||
}
|
||||
log.Printf("✓ Checksum verified: %s", actualChecksum)
|
||||
|
||||
// Step 3: Verify Ed25519 signature
|
||||
log.Printf("[tunturi_ed25519] Step 3: Verifying Ed25519 signature...")
|
||||
if err := verifyBinarySignature(tempBinaryPath, signature); err != nil {
|
||||
return fmt.Errorf("[tunturi_ed25519] signature verification failed: %w", err)
|
||||
}
|
||||
log.Printf("[tunturi_ed25519] ✓ Signature verified")
|
||||
|
||||
// Step 4: Create backup of current binary
|
||||
log.Printf("Step 4: Creating backup...")
|
||||
currentBinaryPath, err := getCurrentBinaryPath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to determine current binary path: %w", err)
|
||||
}
|
||||
|
||||
backupPath := currentBinaryPath + ".bak"
|
||||
var updateSuccess bool = false // Track overall success
|
||||
|
||||
if err := createBackup(currentBinaryPath, backupPath); err != nil {
|
||||
log.Printf("Warning: Failed to create backup: %v", err)
|
||||
} else {
|
||||
// Defer rollback/cleanup logic
|
||||
defer func() {
|
||||
if !updateSuccess {
|
||||
// Rollback on failure
|
||||
log.Printf("[tunturi_ed25519] Rollback: restoring from backup...")
|
||||
if restoreErr := restoreFromBackup(backupPath, currentBinaryPath); restoreErr != nil {
|
||||
log.Printf("[tunturi_ed25519] CRITICAL: Failed to restore backup: %v", restoreErr)
|
||||
} else {
|
||||
log.Printf("[tunturi_ed25519] ✓ Successfully rolled back to backup")
|
||||
}
|
||||
} else {
|
||||
// Clean up backup on success
|
||||
log.Printf("[tunturi_ed25519] ✓ Update successful, cleaning up backup")
|
||||
os.Remove(backupPath)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Step 5: Atomic installation
|
||||
log.Printf("Step 5: Installing new binary...")
|
||||
if err := installNewBinary(tempBinaryPath, currentBinaryPath); err != nil {
|
||||
return fmt.Errorf("failed to install new binary: %w", err)
|
||||
}
|
||||
|
||||
// Step 6: Restart agent service
|
||||
log.Printf("Step 6: Restarting agent service...")
|
||||
if err := restartAgentService(); err != nil {
|
||||
return fmt.Errorf("failed to restart agent: %w", err)
|
||||
}
|
||||
|
||||
// Step 7: Watchdog timer for confirmation
|
||||
log.Printf("Step 7: Starting watchdog for update confirmation...")
|
||||
updateSuccess = waitForUpdateConfirmation(apiClient, cfg, ackTracker, version, 5*time.Minute)
|
||||
success := updateSuccess // Alias for logging below
|
||||
|
||||
finalLogReport := client.LogReport{
|
||||
CommandID: commandID,
|
||||
Action: "update_agent",
|
||||
Result: map[bool]string{true: "success", false: "failure"}[success],
|
||||
Stdout: fmt.Sprintf("Agent update to version %s %s\n", version, map[bool]string{true: "completed successfully", false: "failed"}[success]),
|
||||
Stderr: map[bool]string{true: "", false: "Update verification timeout or restart failure"}[success],
|
||||
ExitCode: map[bool]int{true: 0, false: 1}[success],
|
||||
DurationSeconds: int(time.Since(updateStartTime).Seconds()),
|
||||
Metadata: map[string]string{
|
||||
"subsystem_label": "Agent Update",
|
||||
"subsystem": "agent",
|
||||
"target_version": version,
|
||||
"success": map[bool]string{true: "true", false: "false"}[success],
|
||||
},
|
||||
}
|
||||
|
||||
if err := reportLogWithAck(apiClient, cfg, ackTracker, finalLogReport); err != nil {
|
||||
log.Printf("Failed to report update completion log: %v\n", err)
|
||||
}
|
||||
|
||||
if success {
|
||||
log.Printf("✓ Agent successfully updated to version %s", version)
|
||||
} else {
|
||||
return fmt.Errorf("agent update verification failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper functions for the update process
|
||||
|
||||
func downloadUpdatePackage(downloadURL string) (string, error) {
|
||||
// Download to temporary file
|
||||
tempFile, err := os.CreateTemp("", "redflag-update-*.bin")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create temp file: %w", err)
|
||||
}
|
||||
defer tempFile.Close()
|
||||
|
||||
resp, err := http.Get(downloadURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to download: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("download failed with status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
if _, err := tempFile.ReadFrom(resp.Body); err != nil {
|
||||
return "", fmt.Errorf("failed to write download: %w", err)
|
||||
}
|
||||
|
||||
return tempFile.Name(), nil
|
||||
}
|
||||
|
||||
func computeSHA256(filePath string) (string, error) {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to open file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
hash := sha256.New()
|
||||
if _, err := io.Copy(hash, file); err != nil {
|
||||
return "", fmt.Errorf("failed to compute hash: %w", err)
|
||||
}
|
||||
|
||||
return hex.EncodeToString(hash.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func getCurrentBinaryPath() (string, error) {
|
||||
execPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get executable path: %w", err)
|
||||
}
|
||||
return execPath, nil
|
||||
}
|
||||
|
||||
func createBackup(src, dst string) error {
|
||||
srcFile, err := os.Open(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open source: %w", err)
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
dstFile, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create backup: %w", err)
|
||||
}
|
||||
defer dstFile.Close()
|
||||
|
||||
if _, err := dstFile.ReadFrom(srcFile); err != nil {
|
||||
return fmt.Errorf("failed to copy backup: %w", err)
|
||||
}
|
||||
|
||||
// Ensure backup is executable
|
||||
if err := os.Chmod(dst, 0755); err != nil {
|
||||
return fmt.Errorf("failed to set backup permissions: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func restoreFromBackup(backup, target string) error {
|
||||
// Remove current binary if it exists
|
||||
if _, err := os.Stat(target); err == nil {
|
||||
if err := os.Remove(target); err != nil {
|
||||
return fmt.Errorf("failed to remove current binary: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Copy backup to target
|
||||
return createBackup(backup, target)
|
||||
}
|
||||
|
||||
func installNewBinary(src, dst string) error {
|
||||
// Copy new binary to a temporary location first
|
||||
tempDst := dst + ".new"
|
||||
|
||||
srcFile, err := os.Open(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open source binary: %w", err)
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
dstFile, err := os.Create(tempDst)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp binary: %w", err)
|
||||
}
|
||||
defer dstFile.Close()
|
||||
|
||||
if _, err := dstFile.ReadFrom(srcFile); err != nil {
|
||||
return fmt.Errorf("failed to copy binary: %w", err)
|
||||
}
|
||||
dstFile.Close()
|
||||
|
||||
// Set executable permissions
|
||||
if err := os.Chmod(tempDst, 0755); err != nil {
|
||||
return fmt.Errorf("failed to set binary permissions: %w", err)
|
||||
}
|
||||
|
||||
// Atomic rename
|
||||
if err := os.Rename(tempDst, dst); err != nil {
|
||||
os.Remove(tempDst) // Cleanup temp file
|
||||
return fmt.Errorf("failed to atomically replace binary: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func restartAgentService() error {
|
||||
var cmd *exec.Cmd
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
// Try systemd first
|
||||
cmd = exec.Command("systemctl", "restart", "redflag-agent")
|
||||
if err := cmd.Run(); err == nil {
|
||||
log.Printf("✓ Systemd service restarted")
|
||||
return nil
|
||||
}
|
||||
// Fallback to service command
|
||||
cmd = exec.Command("service", "redflag-agent", "restart")
|
||||
|
||||
case "windows":
|
||||
cmd = exec.Command("sc", "stop", "RedFlagAgent")
|
||||
cmd.Run()
|
||||
cmd = exec.Command("sc", "start", "RedFlagAgent")
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unsupported OS for service restart: %s", runtime.GOOS)
|
||||
}
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to restart service: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("✓ Agent service restarted")
|
||||
return nil
|
||||
}
|
||||
|
||||
func waitForUpdateConfirmation(apiClient *client.Client, cfg *config.Config, ackTracker *acknowledgment.Tracker, expectedVersion string, timeout time.Duration) bool {
|
||||
deadline := time.Now().Add(timeout)
|
||||
pollInterval := 15 * time.Second
|
||||
|
||||
log.Printf("[tunturi_ed25519] Watchdog: waiting for version %s confirmation (timeout: %v)...", expectedVersion, timeout)
|
||||
|
||||
for time.Now().Before(deadline) {
|
||||
// Poll server for current agent version
|
||||
agent, err := apiClient.GetAgent(cfg.AgentID.String())
|
||||
if err != nil {
|
||||
log.Printf("[tunturi_ed25519] Watchdog: failed to poll server: %v (retrying...)", err)
|
||||
time.Sleep(pollInterval)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if the version matches the expected version
|
||||
if agent != nil && agent.CurrentVersion == expectedVersion {
|
||||
log.Printf("[tunturi_ed25519] Watchdog: ✓ Version confirmed: %s", expectedVersion)
|
||||
return true
|
||||
}
|
||||
|
||||
log.Printf("[tunturi_ed25519] Watchdog: Current version: %s, Expected: %s (polling...)",
|
||||
agent.CurrentVersion, expectedVersion)
|
||||
time.Sleep(pollInterval)
|
||||
}
|
||||
|
||||
log.Printf("[tunturi_ed25519] Watchdog: ✗ Timeout after %v - version not confirmed", timeout)
|
||||
log.Printf("[tunturi_ed25519] Rollback initiated")
|
||||
return false
|
||||
}
|
||||
|
||||
// AES-256-GCM decryption helper functions for encrypted update packages
|
||||
|
||||
// deriveKeyFromNonce derives an AES-256 key from a nonce using SHA-256
|
||||
func deriveKeyFromNonce(nonce string) []byte {
|
||||
hash := sha256.Sum256([]byte(nonce))
|
||||
return hash[:] // 32 bytes for AES-256
|
||||
}
|
||||
|
||||
// decryptAES256GCM decrypts data using AES-256-GCM with the provided nonce-derived key
|
||||
func decryptAES256GCM(encryptedData, nonce string) ([]byte, error) {
|
||||
// Derive key from nonce
|
||||
key := deriveKeyFromNonce(nonce)
|
||||
|
||||
// Decode hex data
|
||||
data, err := hex.DecodeString(encryptedData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode hex data: %w", err)
|
||||
}
|
||||
|
||||
// Create AES cipher
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create AES cipher: %w", err)
|
||||
}
|
||||
|
||||
// Create GCM
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create GCM: %w", err)
|
||||
}
|
||||
|
||||
// Check minimum length
|
||||
nonceSize := gcm.NonceSize()
|
||||
if len(data) < nonceSize {
|
||||
return nil, fmt.Errorf("encrypted data too short")
|
||||
}
|
||||
|
||||
// Extract nonce and ciphertext
|
||||
nonceBytes, ciphertext := data[:nonceSize], data[nonceSize:]
|
||||
|
||||
// Decrypt
|
||||
plaintext, err := gcm.Open(nil, nonceBytes, ciphertext, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt: %w", err)
|
||||
}
|
||||
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
// TODO: Integration with system/machine_id.go for key derivation
|
||||
// This stub should be integrated with the existing machine ID system
|
||||
// for more sophisticated key management based on hardware fingerprinting
|
||||
//
|
||||
// Example integration approach:
|
||||
// - Use machine_id.go to generate stable hardware fingerprint
|
||||
// - Combine hardware fingerprint with nonce for key derivation
|
||||
// - Store derived keys securely in memory only
|
||||
// - Implement key rotation support for long-running agents
|
||||
|
||||
// verifyBinarySignature verifies the Ed25519 signature of a binary file
|
||||
func verifyBinarySignature(binaryPath, signatureHex string) error {
|
||||
// Get the server public key from cache
|
||||
publicKey, err := getServerPublicKey()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get server public key: %w", err)
|
||||
}
|
||||
|
||||
// Read the binary content
|
||||
content, err := os.ReadFile(binaryPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read binary: %w", err)
|
||||
}
|
||||
|
||||
// Decode signature from hex
|
||||
signatureBytes, err := hex.DecodeString(signatureHex)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode signature: %w", err)
|
||||
}
|
||||
|
||||
// Verify signature length
|
||||
if len(signatureBytes) != ed25519.SignatureSize {
|
||||
return fmt.Errorf("invalid signature length: expected %d bytes, got %d", ed25519.SignatureSize, len(signatureBytes))
|
||||
}
|
||||
|
||||
// Ed25519 verification
|
||||
valid := ed25519.Verify(ed25519.PublicKey(publicKey), content, signatureBytes)
|
||||
if !valid {
|
||||
return fmt.Errorf("signature verification failed: invalid signature")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getServerPublicKey retrieves the Ed25519 public key from cache
|
||||
// The key is fetched from the server at startup and cached locally
|
||||
func getServerPublicKey() ([]byte, error) {
|
||||
// Load from cache (fetched during agent startup)
|
||||
publicKey, err := loadCachedPublicKeyDirect()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load server public key: %w (hint: key is fetched at agent startup)", err)
|
||||
}
|
||||
|
||||
return publicKey, nil
|
||||
}
|
||||
|
||||
// loadCachedPublicKeyDirect loads the cached public key from the standard location
|
||||
func loadCachedPublicKeyDirect() ([]byte, error) {
|
||||
var keyPath string
|
||||
if runtime.GOOS == "windows" {
|
||||
keyPath = "C:\\ProgramData\\RedFlag\\server_public_key"
|
||||
} else {
|
||||
keyPath = "/etc/aggregator/server_public_key"
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(keyPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("public key not found: %w", err)
|
||||
}
|
||||
|
||||
if len(data) != 32 { // ed25519.PublicKeySize
|
||||
return nil, fmt.Errorf("invalid public key size: expected 32 bytes, got %d", len(data))
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// validateNonce validates the nonce for replay protection
|
||||
func validateNonce(nonceUUIDStr, nonceTimestampStr, nonceSignature string) error {
|
||||
// Parse timestamp
|
||||
nonceTimestamp, err := time.Parse(time.RFC3339, nonceTimestampStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid nonce timestamp format: %w", err)
|
||||
}
|
||||
|
||||
// Check freshness (< 5 minutes)
|
||||
age := time.Since(nonceTimestamp)
|
||||
if age > 5*time.Minute {
|
||||
return fmt.Errorf("nonce expired: age %v > 5 minutes", age)
|
||||
}
|
||||
|
||||
if age < 0 {
|
||||
return fmt.Errorf("nonce timestamp in the future: %v", nonceTimestamp)
|
||||
}
|
||||
|
||||
// Get server public key from cache
|
||||
publicKey, err := getServerPublicKey()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get server public key: %w", err)
|
||||
}
|
||||
|
||||
// Recreate nonce data (must match server format)
|
||||
nonceData := fmt.Sprintf("%s:%d", nonceUUIDStr, nonceTimestamp.Unix())
|
||||
|
||||
// Decode signature
|
||||
signatureBytes, err := hex.DecodeString(nonceSignature)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid nonce signature format: %w", err)
|
||||
}
|
||||
|
||||
if len(signatureBytes) != ed25519.SignatureSize {
|
||||
return fmt.Errorf("invalid nonce signature length: expected %d bytes, got %d",
|
||||
ed25519.SignatureSize, len(signatureBytes))
|
||||
}
|
||||
|
||||
// Verify Ed25519 signature
|
||||
valid := ed25519.Verify(ed25519.PublicKey(publicKey), []byte(nonceData), signatureBytes)
|
||||
if !valid {
|
||||
return fmt.Errorf("invalid nonce signature")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,10 +3,12 @@ module github.com/Fimeg/RedFlag/aggregator-agent
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/denisbrodbeck/machineid v1.0.1
|
||||
github.com/docker/docker v27.4.1+incompatible
|
||||
github.com/go-ole/go-ole v1.3.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/scjalliance/comshim v0.0.0-20250111221056-b2ef9d8d7e0f
|
||||
golang.org/x/sys v0.35.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -31,7 +33,6 @@ require (
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
gotest.tools/v3 v3.5.2 // indirect
|
||||
)
|
||||
|
||||
@@ -8,6 +8,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ=
|
||||
github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/docker v27.4.1+incompatible h1:ZJvcY7gfwHn1JF48PfbyXg7Jyt9ZCWDW+GGXOIxEwp4=
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Fimeg/RedFlag/aggregator-agent/internal/system"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
@@ -21,19 +22,36 @@ type Client struct {
|
||||
http *http.Client
|
||||
RapidPollingEnabled bool
|
||||
RapidPollingUntil time.Time
|
||||
machineID string // Cached machine ID for security binding
|
||||
}
|
||||
|
||||
// NewClient creates a new API client
|
||||
func NewClient(baseURL, token string) *Client {
|
||||
// Get machine ID for security binding (v0.1.22+)
|
||||
machineID, err := system.GetMachineID()
|
||||
if err != nil {
|
||||
// Log warning but don't fail - older servers may not require it
|
||||
fmt.Printf("Warning: Failed to get machine ID: %v\n", err)
|
||||
machineID = "" // Will be handled by server validation
|
||||
}
|
||||
|
||||
return &Client{
|
||||
baseURL: baseURL,
|
||||
token: token,
|
||||
baseURL: baseURL,
|
||||
token: token,
|
||||
machineID: machineID,
|
||||
http: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// addMachineIDHeader adds X-Machine-ID header to authenticated requests (v0.1.22+)
|
||||
func (c *Client) addMachineIDHeader(req *http.Request) {
|
||||
if c.machineID != "" {
|
||||
req.Header.Set("X-Machine-ID", c.machineID)
|
||||
}
|
||||
}
|
||||
|
||||
// GetToken returns the current JWT token
|
||||
func (c *Client) GetToken() string {
|
||||
return c.token
|
||||
@@ -46,13 +64,15 @@ func (c *Client) SetToken(token string) {
|
||||
|
||||
// RegisterRequest is the payload for agent registration
|
||||
type RegisterRequest struct {
|
||||
Hostname string `json:"hostname"`
|
||||
OSType string `json:"os_type"`
|
||||
OSVersion string `json:"os_version"`
|
||||
OSArchitecture string `json:"os_architecture"`
|
||||
AgentVersion string `json:"agent_version"`
|
||||
RegistrationToken string `json:"registration_token,omitempty"` // Fallback method
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
Hostname string `json:"hostname"`
|
||||
OSType string `json:"os_type"`
|
||||
OSVersion string `json:"os_version"`
|
||||
OSArchitecture string `json:"os_architecture"`
|
||||
AgentVersion string `json:"agent_version"`
|
||||
RegistrationToken string `json:"registration_token,omitempty"` // Fallback method
|
||||
MachineID string `json:"machine_id"`
|
||||
PublicKeyFingerprint string `json:"public_key_fingerprint"`
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
}
|
||||
|
||||
// RegisterResponse is returned after successful registration
|
||||
@@ -230,6 +250,7 @@ func (c *Client) GetCommands(agentID uuid.UUID, metrics *SystemMetrics) (*Comman
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+c.token)
|
||||
c.addMachineIDHeader(req) // Security: Validate machine binding (v0.1.22+)
|
||||
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
@@ -297,6 +318,7 @@ func (c *Client) ReportUpdates(agentID uuid.UUID, report UpdateReport) error {
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+c.token)
|
||||
c.addMachineIDHeader(req) // Security: Validate machine binding (v0.1.22+)
|
||||
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
@@ -314,13 +336,14 @@ func (c *Client) ReportUpdates(agentID uuid.UUID, report UpdateReport) error {
|
||||
|
||||
// LogReport represents an execution log
|
||||
type LogReport struct {
|
||||
CommandID string `json:"command_id"`
|
||||
Action string `json:"action"`
|
||||
Result string `json:"result"`
|
||||
Stdout string `json:"stdout"`
|
||||
Stderr string `json:"stderr"`
|
||||
ExitCode int `json:"exit_code"`
|
||||
DurationSeconds int `json:"duration_seconds"`
|
||||
CommandID string `json:"command_id"`
|
||||
Action string `json:"action"`
|
||||
Result string `json:"result"`
|
||||
Stdout string `json:"stdout"`
|
||||
Stderr string `json:"stderr"`
|
||||
ExitCode int `json:"exit_code"`
|
||||
DurationSeconds int `json:"duration_seconds"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// ReportLog sends an execution log to the server
|
||||
@@ -338,6 +361,7 @@ func (c *Client) ReportLog(agentID uuid.UUID, report LogReport) error {
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+c.token)
|
||||
c.addMachineIDHeader(req) // Security: Validate machine binding (v0.1.22+)
|
||||
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
@@ -392,6 +416,7 @@ func (c *Client) ReportDependencies(agentID uuid.UUID, report DependencyReport)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+c.token)
|
||||
c.addMachineIDHeader(req) // Security: Validate machine binding (v0.1.22+)
|
||||
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
@@ -437,6 +462,7 @@ func (c *Client) ReportSystemInfo(agentID uuid.UUID, report SystemInfoReport) er
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+c.token)
|
||||
c.addMachineIDHeader(req) // Security: Validate machine binding (v0.1.22+)
|
||||
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
@@ -474,6 +500,49 @@ func DetectSystem() (osType, osVersion, osArch string) {
|
||||
return
|
||||
}
|
||||
|
||||
// AgentInfo represents agent information from the server
|
||||
type AgentInfo struct {
|
||||
ID string `json:"id"`
|
||||
Hostname string `json:"hostname"`
|
||||
CurrentVersion string `json:"current_version"`
|
||||
OSType string `json:"os_type"`
|
||||
OSVersion string `json:"os_version"`
|
||||
OSArchitecture string `json:"os_architecture"`
|
||||
LastCheckIn string `json:"last_check_in"`
|
||||
}
|
||||
|
||||
// GetAgent retrieves agent information from the server
|
||||
func (c *Client) GetAgent(agentID string) (*AgentInfo, error) {
|
||||
url := fmt.Sprintf("%s/api/v1/agents/%s", c.baseURL, agentID)
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+c.token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
c.addMachineIDHeader(req) // Security: Validate machine binding (v0.1.22+)
|
||||
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to send request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("server returned status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var agent AgentInfo
|
||||
if err := json.NewDecoder(resp.Body).Decode(&agent); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
return &agent, nil
|
||||
}
|
||||
|
||||
// parseOSRelease parses /etc/os-release to get proper distro name
|
||||
func parseOSRelease(data []byte) string {
|
||||
lines := strings.Split(string(data), "\n")
|
||||
|
||||
130
aggregator-agent/internal/crypto/pubkey.go
Normal file
130
aggregator-agent/internal/crypto/pubkey.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// getPublicKeyPath returns the platform-specific path for storing the server's public key
|
||||
func getPublicKeyPath() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return "C:\\ProgramData\\RedFlag\\server_public_key"
|
||||
}
|
||||
return "/etc/aggregator/server_public_key"
|
||||
}
|
||||
|
||||
// PublicKeyResponse represents the server's public key response
|
||||
type PublicKeyResponse struct {
|
||||
PublicKey string `json:"public_key"`
|
||||
Fingerprint string `json:"fingerprint"`
|
||||
Algorithm string `json:"algorithm"`
|
||||
KeySize int `json:"key_size"`
|
||||
}
|
||||
|
||||
// FetchAndCacheServerPublicKey fetches the server's Ed25519 public key and caches it locally
|
||||
// This implements Trust-On-First-Use (TOFU) security model
|
||||
func FetchAndCacheServerPublicKey(serverURL string) (ed25519.PublicKey, error) {
|
||||
// Check if we already have a cached key
|
||||
if cachedKey, err := LoadCachedPublicKey(); err == nil && cachedKey != nil {
|
||||
return cachedKey, nil
|
||||
}
|
||||
|
||||
// Fetch from server
|
||||
resp, err := http.Get(serverURL + "/api/v1/public-key")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch public key from server: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("server returned status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
// Parse response
|
||||
var keyResp PublicKeyResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&keyResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse public key response: %w", err)
|
||||
}
|
||||
|
||||
// Validate algorithm
|
||||
if keyResp.Algorithm != "ed25519" {
|
||||
return nil, fmt.Errorf("unsupported signature algorithm: %s (expected ed25519)", keyResp.Algorithm)
|
||||
}
|
||||
|
||||
// Decode hex public key
|
||||
pubKeyBytes, err := hex.DecodeString(keyResp.PublicKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid public key format: %w", err)
|
||||
}
|
||||
|
||||
if len(pubKeyBytes) != ed25519.PublicKeySize {
|
||||
return nil, fmt.Errorf("invalid public key size: expected %d bytes, got %d",
|
||||
ed25519.PublicKeySize, len(pubKeyBytes))
|
||||
}
|
||||
|
||||
publicKey := ed25519.PublicKey(pubKeyBytes)
|
||||
|
||||
// Cache it for future use
|
||||
if err := cachePublicKey(publicKey); err != nil {
|
||||
// Log warning but don't fail - we have the key in memory
|
||||
fmt.Printf("Warning: Failed to cache public key: %v\n", err)
|
||||
}
|
||||
|
||||
fmt.Printf("✓ Server public key fetched and cached (fingerprint: %s)\n", keyResp.Fingerprint)
|
||||
|
||||
return publicKey, nil
|
||||
}
|
||||
|
||||
// LoadCachedPublicKey loads the cached public key from disk
|
||||
func LoadCachedPublicKey() (ed25519.PublicKey, error) {
|
||||
keyPath := getPublicKeyPath()
|
||||
|
||||
data, err := os.ReadFile(keyPath)
|
||||
if err != nil {
|
||||
return nil, err // File doesn't exist or can't be read
|
||||
}
|
||||
|
||||
if len(data) != ed25519.PublicKeySize {
|
||||
return nil, fmt.Errorf("cached public key has invalid size: %d bytes", len(data))
|
||||
}
|
||||
|
||||
return ed25519.PublicKey(data), nil
|
||||
}
|
||||
|
||||
// cachePublicKey saves the public key to disk
|
||||
func cachePublicKey(publicKey ed25519.PublicKey) error {
|
||||
keyPath := getPublicKeyPath()
|
||||
|
||||
// Ensure directory exists
|
||||
dir := filepath.Dir(keyPath)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create directory: %w", err)
|
||||
}
|
||||
|
||||
// Write public key (read-only for non-root users)
|
||||
if err := os.WriteFile(keyPath, publicKey, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write public key: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPublicKey returns the cached public key or fetches it from the server
|
||||
// This is the main entry point for getting the verification key
|
||||
func GetPublicKey(serverURL string) (ed25519.PublicKey, error) {
|
||||
// Try cached key first
|
||||
if cachedKey, err := LoadCachedPublicKey(); err == nil {
|
||||
return cachedKey, nil
|
||||
}
|
||||
|
||||
// Fetch from server if not cached
|
||||
return FetchAndCacheServerPublicKey(serverURL)
|
||||
}
|
||||
@@ -209,6 +209,13 @@ func (s *redflagService) runAgent() {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if commands response is valid
|
||||
if commands == nil {
|
||||
log.Printf("Check-in successful - no commands received (nil response)")
|
||||
elog.Info(1, "Check-in successful - no commands received (nil response)")
|
||||
continue
|
||||
}
|
||||
|
||||
if len(commands.Commands) == 0 {
|
||||
log.Printf("Check-in successful - no new commands")
|
||||
elog.Info(1, "Check-in successful - no new commands")
|
||||
|
||||
129
aggregator-agent/internal/system/machine_id.go
Normal file
129
aggregator-agent/internal/system/machine_id.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/denisbrodbeck/machineid"
|
||||
)
|
||||
|
||||
// GetMachineID generates a unique machine identifier that persists across reboots
|
||||
func GetMachineID() (string, error) {
|
||||
// Try machineid library first (cross-platform)
|
||||
id, err := machineid.ID()
|
||||
if err == nil && id != "" {
|
||||
// Hash the machine ID for consistency and privacy
|
||||
return hashMachineID(id), nil
|
||||
}
|
||||
|
||||
// Fallback to OS-specific methods
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
return getLinuxMachineID()
|
||||
case "windows":
|
||||
return getWindowsMachineID()
|
||||
case "darwin":
|
||||
return getDarwinMachineID()
|
||||
default:
|
||||
return generateGenericMachineID()
|
||||
}
|
||||
}
|
||||
|
||||
// hashMachineID creates a consistent hash from machine ID
|
||||
func hashMachineID(id string) string {
|
||||
hash := sha256.Sum256([]byte(id))
|
||||
return hex.EncodeToString(hash[:]) // Return full hash for uniqueness
|
||||
}
|
||||
|
||||
// getLinuxMachineID tries multiple sources for Linux machine ID
|
||||
func getLinuxMachineID() (string, error) {
|
||||
// Try /etc/machine-id first (systemd)
|
||||
if id, err := os.ReadFile("/etc/machine-id"); err == nil {
|
||||
idStr := strings.TrimSpace(string(id))
|
||||
if idStr != "" {
|
||||
return hashMachineID(idStr), nil
|
||||
}
|
||||
}
|
||||
|
||||
// Try /var/lib/dbus/machine-id
|
||||
if id, err := os.ReadFile("/var/lib/dbus/machine-id"); err == nil {
|
||||
idStr := strings.TrimSpace(string(id))
|
||||
if idStr != "" {
|
||||
return hashMachineID(idStr), nil
|
||||
}
|
||||
}
|
||||
|
||||
// Try DMI product UUID
|
||||
if id, err := os.ReadFile("/sys/class/dmi/id/product_uuid"); err == nil {
|
||||
idStr := strings.TrimSpace(string(id))
|
||||
if idStr != "" {
|
||||
return hashMachineID(idStr), nil
|
||||
}
|
||||
}
|
||||
|
||||
// Try /etc/hostname as last resort
|
||||
if hostname, err := os.ReadFile("/etc/hostname"); err == nil {
|
||||
hostnameStr := strings.TrimSpace(string(hostname))
|
||||
if hostnameStr != "" {
|
||||
return hashMachineID(hostnameStr + "-linux-fallback"), nil
|
||||
}
|
||||
}
|
||||
|
||||
return generateGenericMachineID()
|
||||
}
|
||||
|
||||
// getWindowsMachineID gets Windows machine ID
|
||||
func getWindowsMachineID() (string, error) {
|
||||
// Try machineid library Windows registry keys first
|
||||
if id, err := machineid.ID(); err == nil && id != "" {
|
||||
return hashMachineID(id), nil
|
||||
}
|
||||
|
||||
// Fallback to generating generic ID
|
||||
return generateGenericMachineID()
|
||||
}
|
||||
|
||||
// getDarwinMachineID gets macOS machine ID
|
||||
func getDarwinMachineID() (string, error) {
|
||||
// Try machineid library platform-specific keys first
|
||||
if id, err := machineid.ID(); err == nil && id != "" {
|
||||
return hashMachineID(id), nil
|
||||
}
|
||||
|
||||
// Fallback to generating generic ID
|
||||
return generateGenericMachineID()
|
||||
}
|
||||
|
||||
// generateGenericMachineID creates a fallback machine ID from available system info
|
||||
func generateGenericMachineID() (string, error) {
|
||||
// Combine hostname with other available info
|
||||
hostname, _ := os.Hostname()
|
||||
if hostname == "" {
|
||||
hostname = "unknown"
|
||||
}
|
||||
|
||||
// Create a reasonably unique ID from available system info
|
||||
idSource := fmt.Sprintf("%s-%s-%s", hostname, runtime.GOOS, runtime.GOARCH)
|
||||
return hashMachineID(idSource), nil
|
||||
}
|
||||
|
||||
// GetEmbeddedPublicKey returns the embedded public key fingerprint
|
||||
// This should be set at build time using ldflags
|
||||
var EmbeddedPublicKey = "not-set-at-build-time"
|
||||
|
||||
// GetPublicKeyFingerprint returns the fingerprint of the embedded public key
|
||||
func GetPublicKeyFingerprint() string {
|
||||
if EmbeddedPublicKey == "not-set-at-build-time" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Return first 8 bytes as fingerprint
|
||||
if len(EmbeddedPublicKey) >= 16 {
|
||||
return EmbeddedPublicKey[:16]
|
||||
}
|
||||
return EmbeddedPublicKey
|
||||
}
|
||||
BIN
aggregator-agent/test-redflag-agent
Executable file
BIN
aggregator-agent/test-redflag-agent
Executable file
Binary file not shown.
Reference in New Issue
Block a user