116 lines
3.7 KiB
Go
116 lines
3.7 KiB
Go
package main
|
|
|
|
// The key this machine's account uses to reach the forge over SSH (novox/hq ADR 0399, ADR 0400; issue 955,
|
|
// area S1). It is made here, on the machine, once: ssh-keygen writes ~/.ssh/mesh_ed25519 only when it is
|
|
// missing, so the private half never leaves the machine and an existing key is never replaced. The public
|
|
// half and its fingerprint go into this module's `keys` state, one entry per account under <node>.<account>,
|
|
// for the forge's provider to register. The tools bundle is the one process of this module, so the keeper
|
|
// runs inside it beside the tools (main.go), the way claude-code reports what it holds.
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// KeyFile is the key's name under ~/.ssh: never one of the operator's own id_* files.
|
|
const KeyFile = "mesh_ed25519"
|
|
|
|
// KeyKind is the kind of key this process makes: the machine's account's own (ADR 0400 §1).
|
|
const KeyKind = "machine"
|
|
|
|
// KeyRetry is how soon it looks again after a failure.
|
|
const KeyRetry = 30 * time.Second
|
|
|
|
// KeyRound is how often the keeper looks again.
|
|
const KeyRound = 10 * time.Minute
|
|
|
|
// KeyEntry is one value of the keys state. It carries the public half only.
|
|
type KeyEntry struct {
|
|
Node string `json:"node"`
|
|
Account string `json:"account"`
|
|
Kind string `json:"kind"`
|
|
Public string `json:"public"`
|
|
Fingerprint string `json:"fingerprint"`
|
|
At string `json:"at"`
|
|
}
|
|
|
|
// KeyPutter is the keys state, as far as the keeper needs it.
|
|
type KeyPutter interface {
|
|
Put(key string, value any) error
|
|
}
|
|
|
|
// KeyKeeper keeps one account's key and its entry.
|
|
type KeyKeeper struct {
|
|
Home, Node, Account string
|
|
Run Runner
|
|
State KeyPutter
|
|
Now func() time.Time
|
|
|
|
reported string // the public half and fingerprint last put
|
|
}
|
|
|
|
func (k *KeyKeeper) path() string { return filepath.Join(k.Home, ".ssh", KeyFile) }
|
|
|
|
// Once makes the key when it is missing and puts the entry when it differs from the one last put.
|
|
func (k *KeyKeeper) Once(ctx context.Context) error {
|
|
if err := k.ensure(ctx); err != nil {
|
|
return err
|
|
}
|
|
raw, err := os.ReadFile(k.path() + ".pub")
|
|
if err != nil {
|
|
return fmt.Errorf("the public half: %w", err)
|
|
}
|
|
pk, err := ParseKeyLine(string(raw))
|
|
if err != nil {
|
|
return fmt.Errorf("%s.pub: %w", k.path(), err)
|
|
}
|
|
public := strings.TrimSpace(string(raw))
|
|
if sig := public + "|" + pk.Fingerprint; sig == k.reported {
|
|
return nil
|
|
}
|
|
entry := KeyEntry{Node: k.Node, Account: k.Account, Kind: KeyKind, Public: public,
|
|
Fingerprint: pk.Fingerprint, At: k.Now().UTC().Format(time.RFC3339)}
|
|
if err := k.State.Put(k.Node+"."+k.Account, entry); err != nil {
|
|
return err
|
|
}
|
|
k.reported = public + "|" + pk.Fingerprint
|
|
return nil
|
|
}
|
|
|
|
// ensure makes the pair when the private half is missing. Never over an existing key.
|
|
func (k *KeyKeeper) ensure(ctx context.Context) error {
|
|
if _, err := os.Lstat(k.path()); err == nil {
|
|
return nil
|
|
} else if !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(k.path()), 0o700); err != nil {
|
|
return err
|
|
}
|
|
r := k.Run(ctx, nil, "ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-C", k.Account+"@"+k.Node+" (mesh)", "-f", k.path())
|
|
if r.Status != 0 || r.Err != "" {
|
|
return fmt.Errorf("ssh-keygen: %s", firstLine(r.Err+" "+r.Stderr))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Keep runs Once now and every KeyRound, reporting a failure through say and trying again at the next round.
|
|
func (k *KeyKeeper) Keep(ctx context.Context, say func(string, ...any)) {
|
|
for {
|
|
pause := KeyRound
|
|
if err := k.Once(ctx); err != nil {
|
|
say("the forge key: %v", err)
|
|
pause = KeyRetry // the state or the bus's grant may arrive after the process
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-time.After(pause):
|
|
}
|
|
}
|
|
}
|