Files
dial/internal/keygen/keygen_test.go
T
bsncubed 8af44dfcd2 Initial commit: dial — interactive SSH host picker
A fast, read-only ~/.ssh/config picker that exec's the system ssh client.
Includes: static high-contrast picker with recency/frequency/name sort and
tag grouping, `dial keygen` (native ed25519/RSA + optional config entry), and
an optional offline YubiKey launch gate with one-time recovery codes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 23:18:23 +10:00

130 lines
3.6 KiB
Go

package keygen
import (
"os"
"path/filepath"
"strings"
"testing"
"golang.org/x/crypto/ssh"
)
func TestGenerateED25519(t *testing.T) {
r, err := Generate(AlgoED25519, "test@dial", nil)
if err != nil {
t.Fatal(err)
}
// Private key must parse back as an OpenSSH key.
if _, err := ssh.ParseRawPrivateKey(r.PrivatePEM); err != nil {
t.Fatalf("private key does not parse: %v", err)
}
// Public line must parse and carry the comment.
pk, comment, _, _, err := ssh.ParseAuthorizedKey(r.PublicLine)
if err != nil {
t.Fatalf("public key does not parse: %v", err)
}
if pk.Type() != ssh.KeyAlgoED25519 {
t.Errorf("expected ed25519, got %s", pk.Type())
}
if comment != "test@dial" {
t.Errorf("comment = %q, want test@dial", comment)
}
}
func TestGenerateWithPassphrase(t *testing.T) {
r, err := Generate(AlgoED25519, "c", []byte("hunter2"))
if err != nil {
t.Fatal(err)
}
if _, err := ssh.ParseRawPrivateKey(r.PrivatePEM); err == nil {
t.Error("encrypted key should not parse without passphrase")
}
if _, err := ssh.ParseRawPrivateKeyWithPassphrase(r.PrivatePEM, []byte("hunter2")); err != nil {
t.Errorf("key should parse with passphrase: %v", err)
}
}
func TestGenerateRSA(t *testing.T) {
r, err := Generate(AlgoRSA, "c", nil)
if err != nil {
t.Fatal(err)
}
pk, _, _, _, err := ssh.ParseAuthorizedKey(r.PublicLine)
if err != nil {
t.Fatal(err)
}
if pk.Type() != ssh.KeyAlgoRSA {
t.Errorf("expected rsa, got %s", pk.Type())
}
}
func TestWriteKeyPairPermsAndNoOverwrite(t *testing.T) {
dir := t.TempDir()
r, _ := Generate(AlgoED25519, "c", nil)
priv, pub, err := WriteKeyPair(dir, "mykey", r)
if err != nil {
t.Fatal(err)
}
fi, _ := os.Stat(priv)
if fi.Mode().Perm() != 0o600 {
t.Errorf("private key perms = %o, want 600", fi.Mode().Perm())
}
pfi, _ := os.Stat(pub)
if pfi.Mode().Perm() != 0o644 {
t.Errorf("public key perms = %o, want 644", pfi.Mode().Perm())
}
// Second write must refuse to overwrite.
if _, _, err := WriteKeyPair(dir, "mykey", r); err == nil {
t.Error("expected refusal to overwrite existing key")
}
}
func TestAppendHostEntryBackupAndDup(t *testing.T) {
dir := t.TempDir()
cfg := filepath.Join(dir, "config")
if err := os.WriteFile(cfg, []byte("Host existing\n HostName e\n"), 0o600); err != nil {
t.Fatal(err)
}
backup, err := AppendHostEntry(cfg, HostEntry{
Alias: "newhost", HostName: "10.0.0.1", User: "ben", Port: "22",
IdentityFile: "~/.ssh/mykey", Tags: []string{"prod"},
})
if err != nil {
t.Fatal(err)
}
if backup == "" {
t.Error("expected a backup path")
}
if _, err := os.Stat(backup); err != nil {
t.Errorf("backup not written: %v", err)
}
b, _ := os.ReadFile(cfg)
got := string(b)
for _, want := range []string{"# tag: prod", "Host newhost", "HostName 10.0.0.1", "IdentitiesOnly yes"} {
if !strings.Contains(got, want) {
t.Errorf("config missing %q\n%s", want, got)
}
}
// Port 22 is the default and should be omitted.
if strings.Contains(got, "Port 22") {
t.Error("default port 22 should not be written")
}
// Appending the same alias must be refused.
if _, err := AppendHostEntry(cfg, HostEntry{Alias: "newhost", HostName: "x"}); err == nil {
t.Error("expected duplicate-alias refusal")
}
}
func TestValidateRejectsInjection(t *testing.T) {
dir := t.TempDir()
cfg := filepath.Join(dir, "config")
if _, err := AppendHostEntry(cfg, HostEntry{Alias: "a b"}); err == nil {
t.Error("alias with space should be rejected")
}
if _, err := AppendHostEntry(cfg, HostEntry{Alias: "ok", HostName: "x\n ProxyCommand evil"}); err == nil {
t.Error("newline injection should be rejected")
}
}