internal/agent/state/hostkey.go
Ref: Size: 2.0 KiB History
package state
import (
"crypto/ed25519"
"crypto/rand"
"encoding/pem"
"fmt"
"os"
"path/filepath"
"strings"
"golang.org/x/crypto/ssh"
)
// HostKey is a guest's SSH host key as it lives on its host: the private half
// in OpenSSH PEM form, which is written into that guest's seed and goes
// nowhere else, and the public half as an authorized_keys line, which is the
// only half that ever travels.
type HostKey struct {
PublicLine string // authorized_keys form, newline-trimmed
PrivatePEM string // OpenSSH PEM
}
// LoadOrCreateHostKey returns the ed25519 host key stored at path, generating
// and persisting one (0600) when there is none there yet.
//
// Load-or-create rather than create is load-bearing, not convenience: between
// generating a key and receiving the certificate for it, the agent may restart
// any number of times. It must come back holding the same key, or the
// certificate the control plane signs is for a key nothing on this host has.
func LoadOrCreateHostKey(path string) (HostKey, error) {
raw, err := os.ReadFile(path)
if err == nil {
return parseHostKey(raw)
}
if !os.IsNotExist(err) {
return HostKey{}, err
}
_, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return HostKey{}, err
}
block, err := ssh.MarshalPrivateKey(priv, "")
if err != nil {
return HostKey{}, err
}
encoded := pem.EncodeToMemory(block)
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
return HostKey{}, err
}
if err := atomicWriteMode(path, encoded, 0o600); err != nil {
return HostKey{}, err
}
return parseHostKey(encoded)
}
// parseHostKey derives both halves from a stored PEM, so a freshly generated
// key and a reloaded one are described by exactly the same code.
func parseHostKey(pemBytes []byte) (HostKey, error) {
signer, err := ssh.ParsePrivateKey(pemBytes)
if err != nil {
return HostKey{}, fmt.Errorf("parse host key: %w", err)
}
line := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(signer.PublicKey())))
return HostKey{PublicLine: line, PrivatePEM: string(pemBytes)}, nil
}