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>
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user