fetch_ml/tests/unit/security/config_integrity_test.go
Jeremie Fraeys 8f9bcef754
test(phase-3): prerequisite security and reproducibility tests
Implement 4 prerequisite test requirements:

- TestConfigIntegrityVerification: Config signing, tamper detection, hash stability
- TestManifestFilenameNonce: Cryptographic nonce generation and filename patterns
- TestGPUDetectionAudit: Structured logging of GPU detection at startup
- TestResourceEnvVarParsing: Resource env var parsing and override behavior

Also update manifest run_manifest.go:
- Add nonce-based filename support to WriteToDir
- Add nonce-based file detection to LoadFromDir
2026-02-23 20:25:26 -05:00

166 lines
4.2 KiB
Go

package security
import (
"os"
"path/filepath"
"testing"
"github.com/jfraeys/fetch_ml/internal/crypto"
"github.com/jfraeys/fetch_ml/internal/worker"
)
// TestConfigIntegrityVerification verifies config file integrity and signature verification.
// This test ensures that config files can be signed and their signatures verified.
func TestConfigIntegrityVerification(t *testing.T) {
t.Run("ConfigLoadWithoutSignature", func(t *testing.T) {
// Create a temp config file
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "config.yaml")
configContent := `
host: localhost
port: 22
max_workers: 4
`
if err := os.WriteFile(configPath, []byte(configContent), 0600); err != nil {
t.Fatalf("failed to write config: %v", err)
}
// Load config without signature verification (default behavior)
cfg, err := worker.LoadConfig(configPath)
if err != nil {
t.Fatalf("LoadConfig failed: %v", err)
}
if cfg.Host != "localhost" {
t.Errorf("host = %q, want localhost", cfg.Host)
}
if cfg.Port != 22 {
t.Errorf("port = %d, want 22", cfg.Port)
}
})
t.Run("ConfigFileTamperingDetection", func(t *testing.T) {
// Create a temp config file
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "config.yaml")
configContent := []byte(`
host: localhost
port: 22
max_workers: 4
`)
if err := os.WriteFile(configPath, configContent, 0600); err != nil {
t.Fatalf("failed to write config: %v", err)
}
// Generate signing keys
publicKey, privateKey, err := crypto.GenerateSigningKeys()
if err != nil {
t.Fatalf("GenerateSigningKeys failed: %v", err)
}
// Create signer
signer, err := crypto.NewManifestSigner(privateKey, "test-key-1")
if err != nil {
t.Fatalf("NewManifestSigner failed: %v", err)
}
// Sign the config content
result, err := signer.SignManifestBytes(configContent)
if err != nil {
t.Fatalf("SignManifestBytes failed: %v", err)
}
// Verify signature against original content
valid, err := crypto.VerifyManifestBytes(configContent, result, publicKey)
if err != nil {
t.Fatalf("VerifyManifestBytes failed: %v", err)
}
if !valid {
t.Error("signature should be valid for original content")
}
// Tamper with the config file
tamperedContent := []byte(`
host: malicious-host
port: 22
max_workers: 4
`)
if err := os.WriteFile(configPath, tamperedContent, 0600); err != nil {
t.Fatalf("failed to write tampered config: %v", err)
}
// Verify signature against tampered content (should fail)
valid, err = crypto.VerifyManifestBytes(tamperedContent, result, publicKey)
if err != nil {
// Expected - signature doesn't match
t.Logf("Expected verification error for tampered content: %v", err)
}
if valid {
t.Error("signature should be invalid for tampered content")
}
})
t.Run("MissingSignatureFile", func(t *testing.T) {
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "config.yaml")
configContent := `
host: localhost
port: 22
`
if err := os.WriteFile(configPath, []byte(configContent), 0600); err != nil {
t.Fatalf("failed to write config: %v", err)
}
// Config loads without signature
cfg, err := worker.LoadConfig(configPath)
if err != nil {
t.Fatalf("LoadConfig should work without signature: %v", err)
}
if cfg == nil {
t.Error("expected config to be loaded")
}
})
}
// TestConfigHashStability verifies that the same config produces the same hash
func TestConfigHashStability(t *testing.T) {
cfg := &worker.Config{
Host: "localhost",
Port: 22,
MaxWorkers: 4,
GPUVendor: "nvidia",
Sandbox: worker.SandboxConfig{
NetworkMode: "none",
SeccompProfile: "default-hardened",
},
}
hash1, err := cfg.ComputeResolvedConfigHash()
if err != nil {
t.Fatalf("ComputeResolvedConfigHash failed: %v", err)
}
hash2, err := cfg.ComputeResolvedConfigHash()
if err != nil {
t.Fatalf("ComputeResolvedConfigHash failed: %v", err)
}
if hash1 != hash2 {
t.Error("same config should produce identical hashes")
}
// Modify config and verify hash changes
cfg.MaxWorkers = 8
hash3, err := cfg.ComputeResolvedConfigHash()
if err != nil {
t.Fatalf("ComputeResolvedConfigHash failed: %v", err)
}
if hash1 == hash3 {
t.Error("different configs should produce different hashes")
}
}