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:
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)
|
||||
}
|
||||
Reference in New Issue
Block a user