229 lines
4.9 KiB
Go
229 lines
4.9 KiB
Go
package keys
|
|
|
|
import (
|
|
"crypto/md5"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
const authorizedKeysPath = "/root/.ssh/authorized_keys"
|
|
const sshConfigPath = "/root/.ssh/config"
|
|
const managedConfigPath = "/root/.ssh/vantage.conf"
|
|
const includeDirective = "Include /root/.ssh/vantage.conf"
|
|
|
|
func ReadAuthorizedKeys() ([]string, error) {
|
|
data, err := os.ReadFile(authorizedKeysPath)
|
|
if os.IsNotExist(err) {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var lines []string
|
|
for _, line := range strings.Split(string(data), "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if line != "" && !strings.HasPrefix(line, "#") {
|
|
lines = append(lines, line)
|
|
}
|
|
}
|
|
return lines, nil
|
|
}
|
|
|
|
func WriteAuthorizedKeys(keys []string) error {
|
|
dir := filepath.Dir(authorizedKeysPath)
|
|
if err := os.MkdirAll(dir, 0700); err != nil {
|
|
return fmt.Errorf("mkdir %s: %w", dir, err)
|
|
}
|
|
|
|
content := strings.Join(keys, "\n")
|
|
if len(keys) > 0 {
|
|
content += "\n"
|
|
}
|
|
|
|
tmpPath := authorizedKeysPath + ".tmp"
|
|
if err := os.WriteFile(tmpPath, []byte(content), 0600); err != nil {
|
|
return fmt.Errorf("write tmp: %w", err)
|
|
}
|
|
|
|
if err := os.Rename(tmpPath, authorizedKeysPath); err != nil {
|
|
os.Remove(tmpPath)
|
|
return fmt.Errorf("rename: %w", err)
|
|
}
|
|
|
|
return os.Chmod(authorizedKeysPath, 0600)
|
|
}
|
|
|
|
func FingerprintLines(lines []string) map[string]bool {
|
|
fp := make(map[string]bool, len(lines))
|
|
for _, line := range lines {
|
|
fp[fingerprint(line)] = true
|
|
}
|
|
return fp
|
|
}
|
|
|
|
func StateChanged(current, desired []string) bool {
|
|
if len(current) != len(desired) {
|
|
return true
|
|
}
|
|
cur := FingerprintLines(current)
|
|
for _, line := range desired {
|
|
if !cur[fingerprint(line)] {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func fingerprint(pubKey string) string {
|
|
parts := strings.Fields(pubKey)
|
|
if len(parts) < 2 {
|
|
return pubKey
|
|
}
|
|
raw, err := base64.StdEncoding.DecodeString(parts[1])
|
|
if err != nil {
|
|
return pubKey
|
|
}
|
|
sum := md5.Sum(raw)
|
|
var pairs []string
|
|
for _, b := range sum {
|
|
pairs = append(pairs, fmt.Sprintf("%02x", b))
|
|
}
|
|
return "MD5:" + strings.Join(pairs, ":")
|
|
}
|
|
|
|
|
|
type KeyGenOptions struct {
|
|
KeyType string
|
|
KeySize int
|
|
Passphrase string
|
|
Comment string
|
|
}
|
|
|
|
|
|
|
|
func GenerateKeyPair(keyPath string, opts KeyGenOptions) (string, error) {
|
|
if err := os.MkdirAll(filepath.Dir(keyPath), 0700); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
keyType := opts.KeyType
|
|
if keyType == "" {
|
|
keyType = "ed25519"
|
|
}
|
|
|
|
args := []string{
|
|
"-t", keyType,
|
|
"-f", keyPath,
|
|
"-N", opts.Passphrase,
|
|
"-C", opts.Comment,
|
|
}
|
|
if opts.KeySize > 0 && keyType != "ed25519" {
|
|
args = append(args, "-b", fmt.Sprintf("%d", opts.KeySize))
|
|
}
|
|
|
|
cmd := exec.Command("ssh-keygen", args...)
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return "", fmt.Errorf("ssh-keygen: %w: %s", err, out)
|
|
}
|
|
|
|
pubData, err := os.ReadFile(keyPath + ".pub")
|
|
if err != nil {
|
|
return "", fmt.Errorf("read pubkey: %w", err)
|
|
}
|
|
return strings.TrimSpace(string(pubData)), nil
|
|
}
|
|
|
|
|
|
|
|
func AddSSHIdentity(keyPath string) error {
|
|
if err := os.MkdirAll(filepath.Dir(sshConfigPath), 0700); err != nil {
|
|
return fmt.Errorf("mkdir .ssh: %w", err)
|
|
}
|
|
|
|
if err := ensureIncludeDirective(); err != nil {
|
|
return err
|
|
}
|
|
|
|
|
|
var existing string
|
|
data, err := os.ReadFile(managedConfigPath)
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return fmt.Errorf("read %s: %w", managedConfigPath, err)
|
|
}
|
|
existing = string(data)
|
|
|
|
line := "IdentityFile " + keyPath
|
|
for _, l := range strings.Split(existing, "\n") {
|
|
if strings.TrimSpace(l) == line {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
if existing != "" && !strings.HasSuffix(existing, "\n") {
|
|
existing += "\n"
|
|
}
|
|
updated := existing + line + "\n"
|
|
|
|
if err := os.WriteFile(managedConfigPath, []byte(updated), 0600); err != nil {
|
|
return fmt.Errorf("write %s: %w", managedConfigPath, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
|
|
func RemoveSSHIdentity(keyPath string) error {
|
|
data, err := os.ReadFile(managedConfigPath)
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("read %s: %w", managedConfigPath, err)
|
|
}
|
|
|
|
line := "IdentityFile " + keyPath
|
|
var kept []string
|
|
for _, l := range strings.Split(strings.TrimRight(string(data), "\n"), "\n") {
|
|
if strings.TrimSpace(l) != line {
|
|
kept = append(kept, l)
|
|
}
|
|
}
|
|
|
|
content := strings.Join(kept, "\n")
|
|
if len(kept) > 0 {
|
|
content += "\n"
|
|
}
|
|
if err := os.WriteFile(managedConfigPath, []byte(content), 0600); err != nil {
|
|
return fmt.Errorf("write %s: %w", managedConfigPath, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
|
|
|
|
|
|
func ensureIncludeDirective() error {
|
|
data, err := os.ReadFile(sshConfigPath)
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return fmt.Errorf("read %s: %w", sshConfigPath, err)
|
|
}
|
|
|
|
for _, l := range strings.Split(string(data), "\n") {
|
|
if strings.TrimSpace(l) == includeDirective {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
|
|
updated := includeDirective + "\n" + string(data)
|
|
if err := os.WriteFile(sshConfigPath, []byte(updated), 0600); err != nil {
|
|
return fmt.Errorf("write %s: %w", sshConfigPath, err)
|
|
}
|
|
return nil
|
|
}
|