a73x

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
}