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:
239
aggregator-server/internal/services/signing.go
Normal file
239
aggregator-server/internal/services/signing.go
Normal file
@@ -0,0 +1,239 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/Fimeg/RedFlag/aggregator-server/internal/models"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// SigningService handles Ed25519 cryptographic operations
|
||||
type SigningService struct {
|
||||
privateKey ed25519.PrivateKey
|
||||
publicKey ed25519.PublicKey
|
||||
}
|
||||
|
||||
// NewSigningService creates a new signing service with the provided private key
|
||||
func NewSigningService(privateKeyHex string) (*SigningService, error) {
|
||||
// Decode private key from hex
|
||||
privateKeyBytes, err := hex.DecodeString(privateKeyHex)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid private key format: %w", err)
|
||||
}
|
||||
|
||||
if len(privateKeyBytes) != ed25519.PrivateKeySize {
|
||||
return nil, fmt.Errorf("invalid private key size: expected %d bytes, got %d", ed25519.PrivateKeySize, len(privateKeyBytes))
|
||||
}
|
||||
|
||||
// Ed25519 private key format: first 32 bytes are seed, next 32 bytes are public key
|
||||
privateKey := ed25519.PrivateKey(privateKeyBytes)
|
||||
publicKey := privateKey.Public().(ed25519.PublicKey)
|
||||
|
||||
return &SigningService{
|
||||
privateKey: privateKey,
|
||||
publicKey: publicKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SignFile signs a file and returns the signature and checksum
|
||||
func (s *SigningService) SignFile(filePath string) (*models.AgentUpdatePackage, error) {
|
||||
// Read the file
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Calculate checksum and sign content
|
||||
content, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read file: %w", err)
|
||||
}
|
||||
|
||||
// Calculate SHA-256 checksum
|
||||
hash := sha256.Sum256(content)
|
||||
checksum := hex.EncodeToString(hash[:])
|
||||
|
||||
// Sign the content
|
||||
signature := ed25519.Sign(s.privateKey, content)
|
||||
|
||||
// Get file info
|
||||
fileInfo, err := file.Stat()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get file info: %w", err)
|
||||
}
|
||||
|
||||
// Determine platform and architecture from file path or use runtime defaults
|
||||
platform, architecture := s.detectPlatformArchitecture(filePath)
|
||||
|
||||
pkg := &models.AgentUpdatePackage{
|
||||
BinaryPath: filePath,
|
||||
Signature: hex.EncodeToString(signature),
|
||||
Checksum: checksum,
|
||||
FileSize: fileInfo.Size(),
|
||||
Platform: platform,
|
||||
Architecture: architecture,
|
||||
CreatedBy: "signing-service",
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
return pkg, nil
|
||||
}
|
||||
|
||||
// VerifySignature verifies a file signature using the embedded public key
|
||||
func (s *SigningService) VerifySignature(content []byte, signatureHex string) (bool, error) {
|
||||
// Decode 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))
|
||||
}
|
||||
|
||||
// Verify signature
|
||||
valid := ed25519.Verify(s.publicKey, content, signature)
|
||||
return valid, nil
|
||||
}
|
||||
|
||||
// GetPublicKey returns the public key in hex format
|
||||
func (s *SigningService) GetPublicKey() string {
|
||||
return hex.EncodeToString(s.publicKey)
|
||||
}
|
||||
|
||||
// GetPublicKeyFingerprint returns a short fingerprint of the public key
|
||||
func (s *SigningService) GetPublicKeyFingerprint() string {
|
||||
// Use first 8 bytes as fingerprint
|
||||
return hex.EncodeToString(s.publicKey[:8])
|
||||
}
|
||||
|
||||
// VerifyFileIntegrity verifies a file's checksum
|
||||
func (s *SigningService) VerifyFileIntegrity(filePath, expectedChecksum string) (bool, error) {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to open file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
content, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to read file: %w", err)
|
||||
}
|
||||
|
||||
hash := sha256.Sum256(content)
|
||||
actualChecksum := hex.EncodeToString(hash[:])
|
||||
|
||||
return actualChecksum == expectedChecksum, nil
|
||||
}
|
||||
|
||||
// detectPlatformArchitecture attempts to detect platform and architecture from file path
|
||||
func (s *SigningService) detectPlatformArchitecture(filePath string) (string, string) {
|
||||
// Default to current runtime
|
||||
platform := runtime.GOOS
|
||||
arch := runtime.GOARCH
|
||||
|
||||
// Map architectures
|
||||
archMap := map[string]string{
|
||||
"amd64": "amd64",
|
||||
"arm64": "arm64",
|
||||
"386": "386",
|
||||
}
|
||||
|
||||
// Try to detect from filename patterns
|
||||
if contains(filePath, "windows") || contains(filePath, ".exe") {
|
||||
platform = "windows"
|
||||
} else if contains(filePath, "linux") {
|
||||
platform = "linux"
|
||||
} else if contains(filePath, "darwin") || contains(filePath, "macos") {
|
||||
platform = "darwin"
|
||||
}
|
||||
|
||||
for archName, archValue := range archMap {
|
||||
if contains(filePath, archName) {
|
||||
arch = archValue
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize architecture names
|
||||
if arch == "amd64" {
|
||||
arch = "amd64"
|
||||
} else if arch == "arm64" {
|
||||
arch = "arm64"
|
||||
}
|
||||
|
||||
return platform, arch
|
||||
}
|
||||
|
||||
// contains is a simple helper for case-insensitive substring checking
|
||||
func contains(s, substr string) bool {
|
||||
return len(s) >= len(substr) && (s == substr ||
|
||||
(len(s) > len(substr) &&
|
||||
(s[:len(substr)] == substr ||
|
||||
s[len(s)-len(substr):] == substr ||
|
||||
findSubstring(s, substr))))
|
||||
}
|
||||
|
||||
// findSubstring is a simple substring finder
|
||||
func findSubstring(s, substr string) bool {
|
||||
for i := 0; i <= len(s)-len(substr); i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// SignNonce signs a nonce (UUID + timestamp) for replay protection
|
||||
func (s *SigningService) SignNonce(nonceUUID uuid.UUID, timestamp time.Time) (string, error) {
|
||||
// Create nonce data: UUID + Unix timestamp as string
|
||||
nonceData := fmt.Sprintf("%s:%d", nonceUUID.String(), timestamp.Unix())
|
||||
|
||||
// Sign the nonce data
|
||||
signature := ed25519.Sign(s.privateKey, []byte(nonceData))
|
||||
|
||||
// Return hex-encoded signature
|
||||
return hex.EncodeToString(signature), nil
|
||||
}
|
||||
|
||||
// VerifyNonce verifies a nonce signature and checks freshness
|
||||
func (s *SigningService) VerifyNonce(nonceUUID uuid.UUID, timestamp time.Time, signatureHex string, maxAge time.Duration) (bool, error) {
|
||||
// Check nonce freshness first
|
||||
if time.Since(timestamp) > maxAge {
|
||||
return false, fmt.Errorf("nonce is too old: %v > %v", time.Since(timestamp), maxAge)
|
||||
}
|
||||
|
||||
// Recreate nonce data
|
||||
nonceData := fmt.Sprintf("%s:%d", nonceUUID.String(), timestamp.Unix())
|
||||
|
||||
// Verify signature
|
||||
valid, err := s.VerifySignature([]byte(nonceData), signatureHex)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to verify nonce signature: %w", err)
|
||||
}
|
||||
|
||||
return valid, nil
|
||||
}
|
||||
|
||||
// TODO: Key rotation implementation
|
||||
// This is a stub for future key rotation functionality
|
||||
// Key rotation should:
|
||||
// 1. Maintain multiple active key pairs with version numbers
|
||||
// 2. Support graceful transition periods (e.g., 30 days)
|
||||
// 3. Store previous keys for signature verification during transition
|
||||
// 4. Batch migration of existing agent fingerprints
|
||||
// 5. Provide monitoring for rotation completion
|
||||
//
|
||||
// Example implementation approach:
|
||||
// - Use database to store multiple key versions with activation timestamps
|
||||
// - Include key version ID in signatures
|
||||
// - Maintain lookup table of previous keys for verification
|
||||
// - Background job to monitor rotation progress
|
||||
Reference in New Issue
Block a user