8af44dfcd2
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>
130 lines
3.6 KiB
Go
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")
|
|
}
|
|
}
|