67 lines
2.0 KiB
Go
67 lines
2.0 KiB
Go
// 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
|
|
}
|