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
166 lines
4.2 KiB
Go
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")
|
|
}
|
|
}
|