package main import ( "crypto/x509" "path/filepath" "sync" "testing" ) func TestHostKeyStoreTOFU(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "known_hosts.json") s := newHostKeyStore(path) // First connection should succeed (trust on first use) if err := s.verify("10.0.0.1:22", "abc123"); err != nil { t.Fatalf("first connect should succeed: %v", err) } // Same key should succeed if err := s.verify("10.0.0.1:22", "abc123"); err != nil { t.Fatalf("same key should succeed: %v", err) } // Different key should fail if err := s.verify("10.0.0.1:22", "different"); err == nil { t.Fatal("changed key should fail") } // New host should succeed if err := s.verify("10.0.0.2:22", "def456"); err != nil { t.Fatalf("new host should succeed: %v", err) } } func TestHostKeyStorePersistence(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "known_hosts.json") s1 := newHostKeyStore(path) _ = s1.verify("host1:22", "fp1") // Load from same file s2 := newHostKeyStore(path) if err := s2.verify("host1:22", "fp1"); err != nil { t.Fatalf("persisted key should match: %v", err) } if err := s2.verify("host1:22", "changed"); err == nil { t.Fatal("changed key should fail after reload") } } func TestHostKeyStoreMissingFile(t *testing.T) { s := newHostKeyStore("/nonexistent/path/known_hosts.json") // Should work in memory even if file can't be read if err := s.verify("host:22", "fp"); err != nil { t.Fatalf("should work without file: %v", err) } } func TestHostKeyStoreConcurrency(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "known_hosts.json") s := newHostKeyStore(path) var wg sync.WaitGroup for i := 0; i < 50; i++ { wg.Add(1) go func() { defer wg.Done() _ = s.verify("host:22", "fp1") }() } wg.Wait() } func TestGetHostKeyStoreDefault(t *testing.T) { // Reset the once for testing origStore := globalHostKeys defer func() { hostKeysOnce = sync.Once{} globalHostKeys = origStore }() hostKeysOnce = sync.Once{} t.Setenv("TOWEROPS_HOST_KEYS_FILE", filepath.Join(t.TempDir(), "test_hosts.json")) store := getHostKeyStore() if store == nil { t.Fatal("expected non-nil store") } } func TestSSHHostKeyCallback(t *testing.T) { // Reset global state origStore := globalHostKeys defer func() { hostKeysOnce = sync.Once{} globalHostKeys = origStore }() hostKeysOnce = sync.Once{} t.Setenv("TOWEROPS_HOST_KEYS_FILE", filepath.Join(t.TempDir(), "hosts.json")) cb := sshHostKeyCallback() if cb == nil { t.Fatal("expected non-nil callback") } } func TestHostKeyStoreSaveError(t *testing.T) { // Use a path in a non-existent directory s := newHostKeyStore("/nonexistent/dir/hosts.json") // verify should not error even if save fails (it logs instead) if err := s.verify("host:22", "fp"); err != nil { t.Fatalf("should succeed even if save fails: %v", err) } } func TestTlsCertFingerprint(t *testing.T) { cert := &x509.Certificate{ Raw: []byte("test certificate data"), } fp := tlsCertFingerprint(cert) if fp == "" { t.Error("expected non-empty fingerprint") } // Verify it's a hex-encoded SHA256 (64 chars) if len(fp) != 64 { t.Errorf("expected 64-char hex fingerprint, got %d chars: %s", len(fp), fp) } // Same input should produce same output fp2 := tlsCertFingerprint(cert) if fp != fp2 { t.Error("fingerprint not deterministic") } }