The key keeper is ssh-client's published package sshkeys, taken by series by the three build modules; Said, KindBuilder, ExecRunnerClean from the Delivery sdk (B5d; ADR 0634 §1; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
@@ -18,6 +18,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-catalog/modules/ssh-client/sshkeys"
|
||||
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
||||
)
|
||||
|
||||
@@ -209,7 +210,7 @@ func (c *Client) Keys(ctx context.Context) ([]Key, error) {
|
||||
k.AgeDays = int(c.Now().Sub(info.ModTime()).Hours() / 24)
|
||||
}
|
||||
if pub, err := os.ReadFile(path + ".pub"); err == nil {
|
||||
if pk, err := forgekey.ParseKeyLine(string(pub)); err == nil {
|
||||
if pk, err := sshkeys.ParseKeyLine(string(pub)); err == nil {
|
||||
k.Type, k.Bits, k.Fingerprint, k.Comment, k.Weak = pk.Type, pk.Bits, pk.Fingerprint, pk.Comment, pk.Weak
|
||||
}
|
||||
} else {
|
||||
@@ -227,7 +228,7 @@ func (c *Client) Keys(ctx context.Context) ([]Key, error) {
|
||||
var derived string
|
||||
k.Passphrase, derived = c.passphrase(ctx, path)
|
||||
if derived != "" {
|
||||
if pk, err := forgekey.ParseKeyLine(derived); err == nil {
|
||||
if pk, err := sshkeys.ParseKeyLine(derived); err == nil {
|
||||
if k.Fingerprint != "" && !k.PublicMissing && pk.Fingerprint != k.Fingerprint {
|
||||
k.PublicMismatch = fmt.Sprintf("the key is %s; its .pub is %s", pk.Fingerprint, k.Fingerprint)
|
||||
}
|
||||
@@ -266,7 +267,7 @@ func (c *Client) passphrase(ctx context.Context, path string) (string, string) {
|
||||
|
||||
// AuthorizedKey is one line of authorized_keys, by fingerprint.
|
||||
type AuthorizedKey struct {
|
||||
forgekey.PublicKey
|
||||
sshkeys.PublicKey
|
||||
Line int `json:"line"`
|
||||
InMesh bool `json:"in_mesh_region,omitempty"`
|
||||
}
|
||||
@@ -311,7 +312,7 @@ func (c *Client) readAuthorized() ([]AuthorizedKey, []int, error) {
|
||||
case t == "" || strings.HasPrefix(t, "#"):
|
||||
continue
|
||||
}
|
||||
k, err := forgekey.ParseKeyLine(t)
|
||||
k, err := sshkeys.ParseKeyLine(t)
|
||||
if err != nil {
|
||||
bad = append(bad, n+1)
|
||||
continue
|
||||
@@ -399,26 +400,26 @@ func atomicWrite(path string, data []byte, mode fs.FileMode) error {
|
||||
// ---- known_hosts -------------------------------------------------------------------------------
|
||||
|
||||
// knownFor is what known_hosts holds for one host (port 22, or [host]:port), by fingerprint.
|
||||
func (c *Client) knownFor(ctx context.Context, host string, port int) ([]forgekey.PublicKey, error) {
|
||||
func (c *Client) knownFor(ctx context.Context, host string, port int) ([]sshkeys.PublicKey, error) {
|
||||
name := host
|
||||
if port != 22 {
|
||||
name = fmt.Sprintf("[%s]:%d", host, port)
|
||||
}
|
||||
file := c.ssh("known_hosts")
|
||||
if _, err := os.Stat(file); os.IsNotExist(err) {
|
||||
return []forgekey.PublicKey{}, nil
|
||||
return []sshkeys.PublicKey{}, nil
|
||||
}
|
||||
r := c.Run(ctx, nil, "ssh-keygen", "-F", name, "-f", file)
|
||||
if r.Err != "" {
|
||||
return nil, named("ssh-keygen -F", r)
|
||||
}
|
||||
// Exit 1 with nothing printed is "not found".
|
||||
keys := []forgekey.PublicKey{}
|
||||
keys := []sshkeys.PublicKey{}
|
||||
for _, l := range strings.Split(r.Stdout, "\n") {
|
||||
if l = strings.TrimSpace(l); l == "" || strings.HasPrefix(l, "#") {
|
||||
continue
|
||||
}
|
||||
if k, err := forgekey.ParseKeyLine(l); err == nil {
|
||||
if k, err := sshkeys.ParseKeyLine(l); err == nil {
|
||||
k.Comment, k.Options = "", ""
|
||||
keys = append(keys, k)
|
||||
}
|
||||
@@ -427,17 +428,17 @@ func (c *Client) knownFor(ctx context.Context, host string, port int) ([]forgeke
|
||||
}
|
||||
|
||||
// scan asks a host for its keys now.
|
||||
func (c *Client) scan(ctx context.Context, host string, port int) ([]forgekey.PublicKey, []string, error) {
|
||||
func (c *Client) scan(ctx context.Context, host string, port int) ([]sshkeys.PublicKey, []string, error) {
|
||||
r := c.Run(ctx, nil, "ssh-keyscan", "-T", "5", "-p", fmt.Sprint(port), host)
|
||||
if r.Err != "" {
|
||||
return nil, nil, named("ssh-keyscan", r)
|
||||
}
|
||||
keys, lines := []forgekey.PublicKey{}, []string{}
|
||||
keys, lines := []sshkeys.PublicKey{}, []string{}
|
||||
for _, l := range strings.Split(r.Stdout, "\n") {
|
||||
if l = strings.TrimSpace(l); l == "" || strings.HasPrefix(l, "#") {
|
||||
continue
|
||||
}
|
||||
if k, err := forgekey.ParseKeyLine(l); err == nil {
|
||||
if k, err := sshkeys.ParseKeyLine(l); err == nil {
|
||||
k.Comment, k.Options = "", ""
|
||||
keys = append(keys, k)
|
||||
lines = append(lines, l)
|
||||
@@ -457,7 +458,7 @@ func orElse(s, def string) string {
|
||||
}
|
||||
|
||||
// compare says how what is known stands against what the host offers now.
|
||||
func compare(known, live []forgekey.PublicKey) string {
|
||||
func compare(known, live []sshkeys.PublicKey) string {
|
||||
if len(known) == 0 {
|
||||
return "not known: the first connection would ask"
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-catalog/modules/ssh-client/sshkeys"
|
||||
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
||||
)
|
||||
|
||||
@@ -60,7 +61,7 @@ func TestTheBundlesKeeperIsForgekeysOfKindMachineWithItsEntryAtNodeAccount(t *te
|
||||
if !ok || len(st.puts) != 1 {
|
||||
t.Fatalf("entries = %v, want one at g14.nox", st.puts)
|
||||
}
|
||||
var e forgekey.KeyEntry
|
||||
var e sshkeys.KeyEntry
|
||||
if err := json.Unmarshal(raw, &e); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
||||
"git.novox.be/novox/mesh-catalog/modules/ssh-client/sshkeys"
|
||||
"github.com/nats-io/nats-server/v2/server"
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
@@ -171,7 +171,7 @@ func TestTheKeyProcessMakesTheKeyAndPutsItsEntryOverTheBus(t *testing.T) {
|
||||
if raw == nil {
|
||||
t.Fatalf("no entry %s in the keys state within 60 s", key)
|
||||
}
|
||||
var e forgekey.KeyEntry
|
||||
var e sshkeys.KeyEntry
|
||||
if err := json.Unmarshal(raw, &e); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -181,7 +181,7 @@ func TestTheKeyProcessMakesTheKeyAndPutsItsEntryOverTheBus(t *testing.T) {
|
||||
if strings.Contains(strings.ToUpper(string(raw)), "PRIVATE") {
|
||||
t.Errorf("the entry carries private material: %s", raw)
|
||||
}
|
||||
info, err := os.Stat(filepath.Join(home, ".ssh", forgekey.KeyFile))
|
||||
info, err := os.Stat(filepath.Join(home, ".ssh", sshkeys.KeyFile))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-catalog/modules/ssh-client/sshkeys"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
||||
)
|
||||
@@ -158,16 +159,16 @@ func startKeeper() {
|
||||
|
||||
// keeperFor is the machine's account's keeper: go/forgekey's, of kind machine (ADR 0400 §1, ADR 0466 §2), whose key
|
||||
// file is ~/.ssh/mesh_ed25519 and whose entry lies at <node>.<account> in the keys state.
|
||||
func keeperFor(home, node, account string, run forgekey.Runner, state forgekey.KeyPutter) *forgekey.KeyKeeper {
|
||||
return &forgekey.KeyKeeper{Home: home, Node: node, Account: account, Kind: forgekey.KindMachine, Run: run, State: state, Now: time.Now}
|
||||
func keeperFor(home, node, account string, run forgekey.Runner, state sshkeys.KeyPutter) *sshkeys.KeyKeeper {
|
||||
return &sshkeys.KeyKeeper{Home: home, Node: node, Account: account, Kind: sshkeys.KindMachine, Run: run, State: state, Now: time.Now}
|
||||
}
|
||||
|
||||
// keepSwept sweeps ~/.ssh (sweep.go) now and every forgekey.KeyRound.
|
||||
// keepSwept sweeps ~/.ssh (sweep.go) now and every sshkeys.KeyRound.
|
||||
func keepSwept(c *Client) {
|
||||
say := func(f string, a ...any) { fmt.Fprintf(os.Stderr, "[ssh-client] "+f+"\n", a...) }
|
||||
for {
|
||||
c.SweepOnce(time.Now(), say)
|
||||
time.Sleep(forgekey.KeyRound)
|
||||
time.Sleep(sshkeys.KeyRound)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-catalog/modules/ssh-client/sshkeys"
|
||||
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
||||
)
|
||||
|
||||
@@ -183,15 +184,15 @@ func TestAPublicHalfThatIsAnotherKeysIsFound(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFingerprintsAreSshKeygens(t *testing.T) {
|
||||
k, err := forgekey.ParseKeyLine("from=\"10.0.0.0/8,192.0.2.1\",no-pty " + edPub)
|
||||
k, err := sshkeys.ParseKeyLine("from=\"10.0.0.0/8,192.0.2.1\",no-pty " + edPub)
|
||||
if err != nil || k.Fingerprint != edFP || k.Bits != 256 || k.Comment != "op@laptop" || !strings.HasPrefix(k.Options, "from=") {
|
||||
t.Fatalf("%+v %v", k, err)
|
||||
}
|
||||
k, _ = forgekey.ParseKeyLine(rsaPub)
|
||||
k, _ = sshkeys.ParseKeyLine(rsaPub)
|
||||
if k.Fingerprint != rsaFP || k.Bits != 2048 || !strings.Contains(k.Weak, "below 3072") {
|
||||
t.Fatalf("%+v", k)
|
||||
}
|
||||
if _, err := forgekey.ParseKeyLine("ssh-ed25519 !!!notbase64"); err == nil {
|
||||
if _, err := sshkeys.ParseKeyLine("ssh-ed25519 !!!notbase64"); err == nil {
|
||||
t.Fatal("garbage accepted")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,20 +22,20 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
||||
"git.novox.be/novox/mesh-catalog/modules/ssh-client/sshkeys"
|
||||
)
|
||||
|
||||
// Declared are the paths under ~/.ssh this module declares (module.json) or its own process makes
|
||||
// (keykeeper.go). A test checks the manifest against this list.
|
||||
var Declared = map[string]bool{
|
||||
"config": true,
|
||||
"known_hosts": true,
|
||||
"config.d": true,
|
||||
"config.d/00-mesh": true,
|
||||
"config.d/10-forge": true,
|
||||
forgekey.KeyFile: true,
|
||||
forgekey.KeyFile + ".pub": true,
|
||||
"authorized_keys": true,
|
||||
"config": true,
|
||||
"known_hosts": true,
|
||||
"config.d": true,
|
||||
"config.d/00-mesh": true,
|
||||
"config.d/10-forge": true,
|
||||
sshkeys.KeyFile: true,
|
||||
sshkeys.KeyFile + ".pub": true,
|
||||
"authorized_keys": true,
|
||||
}
|
||||
|
||||
// SSHReads are the files ssh and sshd read in ~/.ssh by themselves, though this module writes none of
|
||||
@@ -373,7 +373,7 @@ func publicKey(path string) bool {
|
||||
return false
|
||||
}
|
||||
line, _, _ := strings.Cut(string(raw), "\n")
|
||||
if _, err := forgekey.ParseKeyLine(line); err == nil {
|
||||
if _, err := sshkeys.ParseKeyLine(line); err == nil {
|
||||
return true
|
||||
}
|
||||
f := strings.Fields(line)
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
||||
"git.novox.be/novox/mesh-catalog/modules/ssh-client/sshkeys"
|
||||
)
|
||||
|
||||
// ADR 0415, issue 1017: the sweep keeps ~/.ssh to what this module knows and moves the rest out.
|
||||
@@ -75,7 +75,7 @@ func TestSweepKeepsWhatItKnows(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, rel := range []string{"config", "known_hosts", "authorized_keys", "config.d/00-mesh", "config.d/10-forge",
|
||||
forgekey.KeyFile, forgekey.KeyFile + ".pub", "id_ed25519", "id_ed25519.pub", "work_key", "work_key.pub", "work_key-cert.pub", "agent_only.pub", "removed-drone-20261004/id_drone", "authorized_keys2", "id_token"} {
|
||||
sshkeys.KeyFile, sshkeys.KeyFile + ".pub", "id_ed25519", "id_ed25519.pub", "work_key", "work_key.pub", "work_key-cert.pub", "agent_only.pub", "removed-drone-20261004/id_drone", "authorized_keys2", "id_token"} {
|
||||
if !exists(filepath.Join(dir, rel)) {
|
||||
t.Errorf("%s was moved; it is known", rel)
|
||||
}
|
||||
|
||||
@@ -62,6 +62,16 @@ func TestEveryFileTakesItsTextFromATemplate(t *testing.T) {
|
||||
if packs != 1 {
|
||||
t.Errorf("the build packs files/ into %d archives, not one", packs)
|
||||
}
|
||||
// The key keeper is this module's published package, taken by series (ADR 0634 §1).
|
||||
published := false
|
||||
for _, a := range m.Build.Artifacts {
|
||||
if a["name"] == "sshkeys" && a["kind"] == "package" && a["series"] == "0.1" {
|
||||
published = true
|
||||
}
|
||||
}
|
||||
if !published {
|
||||
t.Errorf("no package artifact sshkeys of series 0.1 in the build")
|
||||
}
|
||||
}
|
||||
|
||||
// Issue 955, area S2: the module keeps the keys state and requires the forge's account provision. It declares
|
||||
|
||||
Reference in New Issue
Block a user