847989df75
- git mv cmd/ internal/ pkg/ test/ go.mod go.sum Makefile build.sh docs/ v1/ - Create v1/README.md with V1 documentation - Update root README for V1 + V2 structure - V1 still builds (cd v1 && go build ./cmd/hostkeeper) and 105 tests pass - Root is now clean for V2 development
289 lines
6.7 KiB
Go
289 lines
6.7 KiB
Go
package knownhosts_test
|
|
|
|
import (
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
cryptossh "golang.org/x/crypto/ssh"
|
|
|
|
"git.tukangketik.id/swanadiva/hostkeeper/pkg/knownhosts"
|
|
)
|
|
|
|
func tempDir(t *testing.T) string {
|
|
t.Helper()
|
|
dir, err := os.MkdirTemp("", "knownhosts-test-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
t.Cleanup(func() { os.RemoveAll(dir) })
|
|
return dir
|
|
}
|
|
|
|
func generateTestKey(t *testing.T) cryptossh.PublicKey {
|
|
t.Helper()
|
|
pubKey, _, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
t.Fatalf("Failed to generate key: %v", err)
|
|
}
|
|
sshPubKey, err := cryptossh.NewPublicKey(pubKey)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create SSH public key: %v", err)
|
|
}
|
|
return sshPubKey
|
|
}
|
|
|
|
func generateTestKey2(t *testing.T) cryptossh.PublicKey {
|
|
t.Helper()
|
|
pubKey, _, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
t.Fatalf("Failed to generate key: %v", err)
|
|
}
|
|
sshPubKey, err := cryptossh.NewPublicKey(pubKey)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create SSH public key: %v", err)
|
|
}
|
|
return sshPubKey
|
|
}
|
|
|
|
// 2.1 New creates file on first call
|
|
func TestNewCreatesFile(t *testing.T) {
|
|
dir := tempDir(t)
|
|
kh, err := knownhosts.New(dir)
|
|
if err != nil {
|
|
t.Fatalf("New failed: %v", err)
|
|
}
|
|
if kh == nil {
|
|
t.Fatal("New should return non-nil KnownHosts")
|
|
}
|
|
// File should be created after first Add+Save
|
|
}
|
|
|
|
// 2.2 New loads existing
|
|
func TestNewLoadsExisting(t *testing.T) {
|
|
dir := tempDir(t)
|
|
key := generateTestKey(t)
|
|
|
|
// Create and add a host
|
|
kh1, _ := knownhosts.New(dir)
|
|
_ = kh1.Add("example.com", 22, key)
|
|
|
|
// Load again
|
|
kh2, err := knownhosts.New(dir)
|
|
if err != nil {
|
|
t.Fatalf("New failed: %v", err)
|
|
}
|
|
stored := kh2.Get("example.com", 22)
|
|
if stored == nil {
|
|
t.Fatal("Should load existing host from file")
|
|
}
|
|
}
|
|
|
|
// 2.3 Add new host
|
|
func TestAddNewHost(t *testing.T) {
|
|
dir := tempDir(t)
|
|
key := generateTestKey(t)
|
|
|
|
kh, _ := knownhosts.New(dir)
|
|
err := kh.Add("example.com", 22, key)
|
|
if err != nil {
|
|
t.Fatalf("Add failed: %v", err)
|
|
}
|
|
|
|
stored := kh.Get("example.com", 22)
|
|
if stored == nil {
|
|
t.Fatal("Get should return the added host")
|
|
}
|
|
if stored.Hostname != "example.com" {
|
|
t.Errorf("Hostname = %q, want %q", stored.Hostname, "example.com")
|
|
}
|
|
if stored.Port != 22 {
|
|
t.Errorf("Port = %d, want 22", stored.Port)
|
|
}
|
|
}
|
|
|
|
// 2.4 Add duplicate — no error, no duplicate
|
|
func TestAddDuplicate(t *testing.T) {
|
|
dir := tempDir(t)
|
|
key := generateTestKey(t)
|
|
|
|
kh, _ := knownhosts.New(dir)
|
|
_ = kh.Add("example.com", 22, key)
|
|
err := kh.Add("example.com", 22, key)
|
|
if err != nil {
|
|
t.Fatalf("Add duplicate should not error: %v", err)
|
|
}
|
|
}
|
|
|
|
// 2.5 Get existing host
|
|
func TestGetExisting(t *testing.T) {
|
|
dir := tempDir(t)
|
|
key := generateTestKey(t)
|
|
|
|
kh, _ := knownhosts.New(dir)
|
|
_ = kh.Add("example.com", 22, key)
|
|
|
|
stored := kh.Get("example.com", 22)
|
|
if stored == nil {
|
|
t.Fatal("Get should return existing host")
|
|
}
|
|
if stored.Hostname != "example.com" {
|
|
t.Errorf("Hostname = %q, want %q", stored.Hostname, "example.com")
|
|
}
|
|
}
|
|
|
|
// 2.6 Get non-existent host
|
|
func TestGetNonExistent(t *testing.T) {
|
|
dir := tempDir(t)
|
|
kh, _ := knownhosts.New(dir)
|
|
|
|
stored := kh.Get("unknown.com", 22)
|
|
if stored != nil {
|
|
t.Error("Get should return nil for non-existent host")
|
|
}
|
|
}
|
|
|
|
// 2.7 Remove existing host
|
|
func TestRemoveExisting(t *testing.T) {
|
|
dir := tempDir(t)
|
|
key := generateTestKey(t)
|
|
|
|
kh, _ := knownhosts.New(dir)
|
|
_ = kh.Add("example.com", 22, key)
|
|
|
|
err := kh.Remove("example.com", 22)
|
|
if err != nil {
|
|
t.Fatalf("Remove failed: %v", err)
|
|
}
|
|
|
|
stored := kh.Get("example.com", 22)
|
|
if stored != nil {
|
|
t.Error("Get should return nil after Remove")
|
|
}
|
|
}
|
|
|
|
// 2.8 Remove non-existent host — no error
|
|
func TestRemoveNonExistent(t *testing.T) {
|
|
dir := tempDir(t)
|
|
kh, _ := knownhosts.New(dir)
|
|
|
|
err := kh.Remove("unknown.com", 22)
|
|
if err != nil {
|
|
t.Fatalf("Remove non-existent should not error: %v", err)
|
|
}
|
|
}
|
|
|
|
// 2.9 Verify unknown host — (false, nil) TOFU
|
|
func TestVerifyUnknown(t *testing.T) {
|
|
dir := tempDir(t)
|
|
key := generateTestKey(t)
|
|
|
|
kh, _ := knownhosts.New(dir)
|
|
matches, stored := kh.Verify("unknown.com", 22, key)
|
|
if matches {
|
|
t.Error("Verify should return false for unknown host")
|
|
}
|
|
if stored != nil {
|
|
t.Error("Verify should return nil HostKey for unknown host")
|
|
}
|
|
}
|
|
|
|
// 2.10 Verify known host, matching key — (true, hostKey)
|
|
func TestVerifyKnownMatch(t *testing.T) {
|
|
dir := tempDir(t)
|
|
key := generateTestKey(t)
|
|
|
|
kh, _ := knownhosts.New(dir)
|
|
_ = kh.Add("example.com", 22, key)
|
|
|
|
matches, stored := kh.Verify("example.com", 22, key)
|
|
if !matches {
|
|
t.Error("Verify should return true for matching key")
|
|
}
|
|
if stored == nil {
|
|
t.Error("Verify should return stored HostKey")
|
|
}
|
|
}
|
|
|
|
// 2.11 Verify known host, mismatched key — (false, hostKey) MITM
|
|
func TestVerifyKnownMismatch(t *testing.T) {
|
|
dir := tempDir(t)
|
|
key1 := generateTestKey(t)
|
|
key2 := generateTestKey2(t)
|
|
|
|
kh, _ := knownhosts.New(dir)
|
|
_ = kh.Add("example.com", 22, key1)
|
|
|
|
matches, stored := kh.Verify("example.com", 22, key2)
|
|
if matches {
|
|
t.Error("Verify should return false for mismatched key (MITM)")
|
|
}
|
|
if stored == nil {
|
|
t.Error("Verify should return stored HostKey for mismatch")
|
|
}
|
|
}
|
|
|
|
// 2.12 HostKeyCallback — autoAdd=true adds unknown hosts
|
|
func TestHostKeyCallbackAutoAdd(t *testing.T) {
|
|
dir := tempDir(t)
|
|
key := generateTestKey(t)
|
|
|
|
kh, _ := knownhosts.New(dir)
|
|
callback := kh.HostKeyCallback(true)
|
|
|
|
// Simulate host key check via callback
|
|
// HostKeyCallback expects net.Addr, so we create a fake one
|
|
addr := &fakeAddr{addr: "192.168.1.1:22"}
|
|
err := callback("example.com", addr, key)
|
|
if err != nil {
|
|
t.Fatalf("HostKeyCallback with autoAdd failed: %v", err)
|
|
}
|
|
|
|
// Verify host was added
|
|
stored := kh.Get("example.com", 22)
|
|
if stored == nil {
|
|
t.Error("HostKeyCallback should add unknown host when autoAdd=true")
|
|
}
|
|
}
|
|
|
|
// 2.13 Persistence — Add → Save → New → Get
|
|
func TestPersistence(t *testing.T) {
|
|
dir := tempDir(t)
|
|
key := generateTestKey(t)
|
|
|
|
kh1, _ := knownhosts.New(dir)
|
|
_ = kh1.Add("example.com", 22, key)
|
|
|
|
// Create new instance from same directory
|
|
kh2, err := knownhosts.New(dir)
|
|
if err != nil {
|
|
t.Fatalf("New failed: %v", err)
|
|
}
|
|
stored := kh2.Get("example.com", 22)
|
|
if stored == nil {
|
|
t.Fatal("Host should persist across New() calls")
|
|
}
|
|
}
|
|
|
|
// 2.14 Corrupted file → error
|
|
func TestCorruptedFile(t *testing.T) {
|
|
dir := tempDir(t)
|
|
path := filepath.Join(dir, "known_hosts")
|
|
_ = os.WriteFile(path, []byte("not valid json {{{"), 0600)
|
|
|
|
_, err := knownhosts.New(dir)
|
|
if err == nil {
|
|
t.Error("New should return error for corrupted file")
|
|
}
|
|
}
|
|
|
|
// fakeAddr implements net.Addr for testing
|
|
type fakeAddr struct {
|
|
addr string
|
|
}
|
|
|
|
func (f *fakeAddr) Network() string { return "tcp" }
|
|
func (f *fakeAddr) String() string { return f.addr }
|