test(proxmox,cli): end-to-end trust-surface integration tests (T02.10, REQ-058, REQ-059)
---ci--- project: orca phase: 2 milestone: v0.8 status: execute ---/ci---
This commit is contained in:
@@ -784,3 +784,265 @@ func TestResetHostKey_EmptyHostErrors(t *testing.T) {
|
||||
t.Error("expected error for empty host, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// bootstrapE2ESetup wires the real dialer against a fake SSH server so
|
||||
// the full HostKeyCallback path (pinned or TOFU) runs end-to-end through
|
||||
// BootstrapProxmox. Returns the host, port, and server (for fingerprint
|
||||
// computation). The known_hosts file is created empty in the temp
|
||||
// ORCA_HOME.
|
||||
func bootstrapE2ESetup(t *testing.T) (srv *fakeSSHServer, host, port string) {
|
||||
t.Helper()
|
||||
srv = newFakeSSHServer(t)
|
||||
t.Cleanup(srv.close)
|
||||
home := t.TempDir()
|
||||
t.Setenv("ORCA_HOME", home)
|
||||
if err := os.WriteFile(filepath.Join(home, "known_hosts"), []byte{}, 0o600); err != nil {
|
||||
t.Fatalf("create known_hosts: %v", err)
|
||||
}
|
||||
orig := sshDialer
|
||||
t.Cleanup(func() { sshDialer = orig })
|
||||
origRunner := sessionRunner
|
||||
t.Cleanup(func() { sessionRunner = origRunner })
|
||||
sessionRunner = nil
|
||||
sshDialer = defaultSSHDialer{}
|
||||
host, port, _ = net.SplitHostPort(srv.addr())
|
||||
return srv, host, port
|
||||
}
|
||||
|
||||
// TestBootstrapE2E_PinnedFingerprintCorrect verifies that
|
||||
// --host-key-fingerprint with the correct pin (T02.10 case 1) succeeds
|
||||
// end-to-end and Result.HostKeyFingerprint equals the pinned value.
|
||||
func TestBootstrapE2E_PinnedFingerprintCorrect(t *testing.T) {
|
||||
srv, host, port := bootstrapE2ESetup(t)
|
||||
hostKey := srv.hostPublicKey()
|
||||
if hostKey == nil {
|
||||
t.Fatal("server host key is nil")
|
||||
}
|
||||
pin := security.SSHFingerprintSHA256(hostKey)
|
||||
portNum, _ := strconv.Atoi(port)
|
||||
|
||||
result, err := BootstrapProxmox(t.Context(), Options{
|
||||
Host: host,
|
||||
Password: "pw",
|
||||
SSHPort: portNum,
|
||||
HostKeyFingerprint: pin,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("BootstrapProxmox with correct pin: %v", err)
|
||||
}
|
||||
if result.HostKeyFingerprint != pin {
|
||||
t.Errorf("Result.HostKeyFingerprint = %q, want %q (pinned value)",
|
||||
result.HostKeyFingerprint, pin)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBootstrapE2E_PinnedFingerprintWrong verifies that
|
||||
// --host-key-fingerprint with a wrong pin (T02.10 case 2) fails fast
|
||||
// with the REQ-058 mismatch error, before any SSH session commands run.
|
||||
func TestBootstrapE2E_PinnedFingerprintWrong(t *testing.T) {
|
||||
_, host, port := bootstrapE2ESetup(t)
|
||||
portNum, _ := strconv.Atoi(port)
|
||||
wrong := "SHA256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
|
||||
|
||||
_, err := BootstrapProxmox(t.Context(), Options{
|
||||
Host: host,
|
||||
Password: "pw",
|
||||
SSHPort: portNum,
|
||||
HostKeyFingerprint: wrong,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for wrong pin, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "REQ-058") {
|
||||
t.Errorf("error should mention REQ-058, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBootstrapE2E_TOFUFirstConnectCapturesKey verifies that with no
|
||||
// --host-key-fingerprint on a first connect (empty known_hosts) (T02.10
|
||||
// case 3) the TOFU callback captures the key, writes known_hosts, and
|
||||
// bootstrap succeeds — exercised end-to-end through BootstrapProxmox.
|
||||
func TestBootstrapE2E_TOFUFirstConnectCapturesKey(t *testing.T) {
|
||||
srv, host, port := bootstrapE2ESetup(t)
|
||||
hostKey := srv.hostPublicKey()
|
||||
if hostKey == nil {
|
||||
t.Fatal("server host key is nil")
|
||||
}
|
||||
portNum, _ := strconv.Atoi(port)
|
||||
home := os.Getenv("ORCA_HOME")
|
||||
knownHostsPath := filepath.Join(home, "known_hosts")
|
||||
|
||||
before, _ := os.ReadFile(knownHostsPath)
|
||||
if len(before) != 0 {
|
||||
t.Fatalf("precondition: known_hosts not empty: %q", before)
|
||||
}
|
||||
|
||||
result, err := BootstrapProxmox(t.Context(), Options{
|
||||
Host: host,
|
||||
Password: "pw",
|
||||
SSHPort: portNum,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("BootstrapProxmox first connect: %v", err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(knownHostsPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read known_hosts: %v", err)
|
||||
}
|
||||
if len(data) == 0 {
|
||||
t.Fatal("known_hosts empty — TOFU did not capture the key end-to-end")
|
||||
}
|
||||
expectedFP := security.SSHFingerprintSHA256(hostKey)
|
||||
if result.HostKeyFingerprint != expectedFP {
|
||||
t.Errorf("Result.HostKeyFingerprint = %q, want %q", result.HostKeyFingerprint, expectedFP)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBootstrapE2E_TOFUSecondConnectMatches verifies that a second
|
||||
// connect (known_hosts already has the key from the first connect)
|
||||
// (T02.10 case 4) matches and succeeds end-to-end.
|
||||
func TestBootstrapE2E_TOFUSecondConnectMatches(t *testing.T) {
|
||||
srv, host, port := bootstrapE2ESetup(t)
|
||||
portNum, _ := strconv.Atoi(port)
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
sessionRunner = nil
|
||||
if _, err := BootstrapProxmox(t.Context(), Options{
|
||||
Host: host,
|
||||
Password: "pw",
|
||||
SSHPort: portNum,
|
||||
}); err != nil {
|
||||
t.Fatalf("bootstrap run %d: %v", i+1, err)
|
||||
}
|
||||
}
|
||||
_ = srv
|
||||
}
|
||||
|
||||
// TestBootstrapE2E_TOFUMismatchFails verifies that when known_hosts has
|
||||
// a different key (T02.10 case 5) the second connect fails with a
|
||||
// mismatch (MITM detection) — end-to-end through BootstrapProxmox.
|
||||
func TestBootstrapE2E_TOFUMismatchFails(t *testing.T) {
|
||||
srv, host, port := bootstrapE2ESetup(t)
|
||||
hostKey := srv.hostPublicKey()
|
||||
if hostKey == nil {
|
||||
t.Fatal("server host key is nil")
|
||||
}
|
||||
portNum, _ := strconv.Atoi(port)
|
||||
home := os.Getenv("ORCA_HOME")
|
||||
knownHostsPath := filepath.Join(home, "known_hosts")
|
||||
|
||||
altPub, _, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatalf("ed25519 gen: %v", err)
|
||||
}
|
||||
altKey, err := ssh.NewPublicKey(altPub)
|
||||
if err != nil {
|
||||
t.Fatalf("new pub: %v", err)
|
||||
}
|
||||
addr := host + ":" + port
|
||||
altLine := knownhosts.Line([]string{knownhosts.Normalize(addr)}, altKey)
|
||||
if err := os.WriteFile(knownHostsPath, []byte(altLine+"\n"), 0o600); err != nil {
|
||||
t.Fatalf("write known_hosts: %v", err)
|
||||
}
|
||||
|
||||
_, err = BootstrapProxmox(t.Context(), Options{
|
||||
Host: host,
|
||||
Password: "pw",
|
||||
SSHPort: portNum,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected MITM/mismatch error, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "ssh dial") {
|
||||
t.Errorf("error should mention ssh dial, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBootstrapE2E_PrePopulatedKnownHostsMatches verifies the v0.6→v0.8
|
||||
// migration path (T02.10 case 7): a known_hosts entry written by a prior
|
||||
// join (simulating a v0.6 install) is matched on second-connect without
|
||||
// re-capture, end-to-end through BootstrapProxmox.
|
||||
func TestBootstrapE2E_PrePopulatedKnownHostsMatches(t *testing.T) {
|
||||
srv, host, port := bootstrapE2ESetup(t)
|
||||
hostKey := srv.hostPublicKey()
|
||||
if hostKey == nil {
|
||||
t.Fatal("server host key is nil")
|
||||
}
|
||||
portNum, _ := strconv.Atoi(port)
|
||||
home := os.Getenv("ORCA_HOME")
|
||||
knownHostsPath := filepath.Join(home, "known_hosts")
|
||||
|
||||
addr := host + ":" + port
|
||||
preLine := knownhosts.Line([]string{knownhosts.Normalize(addr)}, hostKey)
|
||||
if err := os.WriteFile(knownHostsPath, []byte(preLine+"\n"), 0o600); err != nil {
|
||||
t.Fatalf("write known_hosts: %v", err)
|
||||
}
|
||||
|
||||
result, err := BootstrapProxmox(t.Context(), Options{
|
||||
Host: host,
|
||||
Password: "pw",
|
||||
SSHPort: portNum,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("BootstrapProxmox on pre-populated known_hosts: %v", err)
|
||||
}
|
||||
expectedFP := security.SSHFingerprintSHA256(hostKey)
|
||||
if result.HostKeyFingerprint != expectedFP {
|
||||
t.Errorf("Result.HostKeyFingerprint = %q, want %q", result.HostKeyFingerprint, expectedFP)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBootstrapE2E_KeyResetThenRePin verifies T02.10 case 6: after
|
||||
// ResetHostKey removes the known_hosts entry, the next BootstrapProxmox
|
||||
// connect re-pins the key via TOFU and succeeds end-to-end. The reset
|
||||
// target is the known_hosts entry key (host:port, normalized), which
|
||||
// matches how the cli resolves the host from a proxmox node's address
|
||||
// for non-default ports.
|
||||
func TestBootstrapE2E_KeyResetThenRePin(t *testing.T) {
|
||||
srv, host, port := bootstrapE2ESetup(t)
|
||||
portNum, _ := strconv.Atoi(port)
|
||||
home := os.Getenv("ORCA_HOME")
|
||||
knownHostsPath := filepath.Join(home, "known_hosts")
|
||||
addr := host + ":" + port
|
||||
|
||||
// First connect: TOFU captures + writes known_hosts.
|
||||
sessionRunner = nil
|
||||
if _, err := BootstrapProxmox(t.Context(), Options{
|
||||
Host: host,
|
||||
Password: "pw",
|
||||
SSHPort: portNum,
|
||||
}); err != nil {
|
||||
t.Fatalf("first bootstrap: %v", err)
|
||||
}
|
||||
before, _ := os.ReadFile(knownHostsPath)
|
||||
if len(before) == 0 {
|
||||
t.Fatal("precondition: known_hosts empty after first connect")
|
||||
}
|
||||
|
||||
// Reset: known_hosts entry removed. Pass the full addr (host:port)
|
||||
// so Normalize produces the same bracketed form the TOFU callback
|
||||
// wrote for a non-default port.
|
||||
if err := ResetHostKey(addr); err != nil {
|
||||
t.Fatalf("ResetHostKey: %v", err)
|
||||
}
|
||||
after, _ := os.ReadFile(knownHostsPath)
|
||||
if strings.Contains(string(after), knownhosts.Normalize(addr)) {
|
||||
t.Fatalf("known_hosts still contains host after reset: %q", after)
|
||||
}
|
||||
|
||||
// Next connect re-pins via TOFU + succeeds.
|
||||
sessionRunner = nil
|
||||
if _, err := BootstrapProxmox(t.Context(), Options{
|
||||
Host: host,
|
||||
Password: "pw",
|
||||
SSHPort: portNum,
|
||||
}); err != nil {
|
||||
t.Fatalf("re-pin bootstrap after reset: %v", err)
|
||||
}
|
||||
rePinned, _ := os.ReadFile(knownHostsPath)
|
||||
if !strings.Contains(string(rePinned), knownhosts.Normalize(addr)) {
|
||||
t.Fatalf("known_hosts not re-populated on next connect: %q", rePinned)
|
||||
}
|
||||
_ = srv
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user