// Package keymgr generates per-user SSH keypairs and packages them, together // with a ready-to-use client config and Windows-oriented instructions, into // a password-protected 7z archive for delivery. // // The private key only ever exists on disk for the lifetime of one export // operation: generated into a temp directory, packed into the archive, then // securely wiped (see Wipe in pack.go). It is never written into the state // store and therefore never ends up in a backup. package keymgr import ( "fmt" "os" "os/exec" "path/filepath" "strings" ) // KeyPair is the result of generating one user's SSH keypair. type KeyPair struct { PrivateKeyPath string PublicKeyLine string // full "ssh-ed25519 AAAA... comment" line for authorized_keys Fingerprint string // "SHA256:xxxxx" as reported by ssh-keygen -lf } // GeneratePair creates a new passphrase-less ed25519 keypair for username in // dir. The key itself has no passphrase because the archive it ships in is // already encrypted (see pack.go); a second passphrase would only add // friction without a corresponding threat it defends against once the // archive's own encryption and the server-side no-shell restriction are in // place. func GeneratePair(dir, username string) (*KeyPair, error) { priv := filepath.Join(dir, username+"_jump_key") pub := priv + ".pub" cmd := exec.Command("ssh-keygen", "-t", "ed25519", "-N", "", // no passphrase "-C", username+"@jump", "-f", priv, ) if out, err := cmd.CombinedOutput(); err != nil { return nil, fmt.Errorf("ssh-keygen: %w: %s", err, out) } pubData, err := os.ReadFile(pub) if err != nil { return nil, fmt.Errorf("read public key: %w", err) } fpOut, err := exec.Command("ssh-keygen", "-lf", pub).CombinedOutput() if err != nil { return nil, fmt.Errorf("fingerprint: %w: %s", err, fpOut) } fields := strings.Fields(string(fpOut)) fingerprint := "" if len(fields) >= 2 { fingerprint = fields[1] } return &KeyPair{ PrivateKeyPath: priv, PublicKeyLine: strings.TrimSpace(string(pubData)), Fingerprint: fingerprint, }, nil }