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