141 lines
4.4 KiB
Go
141 lines
4.4 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// Issue 955, area S1: the key the machine's account makes for the forge, and its entry in the keys state.
|
|
|
|
type fakeState struct{ puts map[string][]byte }
|
|
|
|
func (f *fakeState) Put(key string, value any) error {
|
|
raw, _ := json.Marshal(value)
|
|
if f.puts == nil {
|
|
f.puts = map[string][]byte{}
|
|
}
|
|
f.puts[key] = raw
|
|
return nil
|
|
}
|
|
|
|
// ed25519Line is a public key line with a well-formed ed25519 blob.
|
|
func ed25519Line(seed byte, comment string) string {
|
|
blob := []byte{}
|
|
for _, part := range [][]byte{[]byte("ssh-ed25519"), make([]byte, 32)} {
|
|
part2 := append([]byte(nil), part...)
|
|
if len(part2) == 32 {
|
|
part2[0] = seed
|
|
}
|
|
l := make([]byte, 4)
|
|
binary.BigEndian.PutUint32(l, uint32(len(part2)))
|
|
blob = append(append(blob, l...), part2...)
|
|
}
|
|
return "ssh-ed25519 " + base64.StdEncoding.EncodeToString(blob) + " " + comment
|
|
}
|
|
|
|
// keygenRunner stands in for ssh-keygen -t ed25519: it writes the pair the command names.
|
|
func keygenRunner(calls *[][]string) Runner {
|
|
return func(ctx context.Context, env []string, name string, args ...string) Ran {
|
|
*calls = append(*calls, append([]string{name}, args...))
|
|
var file, comment string
|
|
for i, a := range args {
|
|
switch a {
|
|
case "-f":
|
|
file = args[i+1]
|
|
case "-C":
|
|
comment = args[i+1]
|
|
}
|
|
}
|
|
if name != "ssh-keygen" || file == "" {
|
|
return Ran{Status: 1, Stderr: "unexpected"}
|
|
}
|
|
os.WriteFile(file, []byte("-----BEGIN OPENSSH PRIVATE KEY-----\nPRIVATE-SECRET\n"), 0o600)
|
|
os.WriteFile(file+".pub", []byte(ed25519Line(7, comment)+"\n"), 0o644)
|
|
return Ran{}
|
|
}
|
|
}
|
|
|
|
func newKeeper(t *testing.T, home string, st *fakeState, calls *[][]string) *KeyKeeper {
|
|
return &KeyKeeper{Home: home, Node: "g14", Account: "nox", Run: keygenRunner(calls), State: st,
|
|
Now: func() time.Time { return time.Date(2026, 10, 16, 12, 0, 0, 0, time.UTC) }}
|
|
}
|
|
|
|
func TestKeeperMakesTheKeyAndWritesItsEntry(t *testing.T) {
|
|
home := t.TempDir()
|
|
st, calls := &fakeState{}, &[][]string{}
|
|
if err := newKeeper(t, home, st, calls).Once(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw, ok := st.puts["g14.nox"]
|
|
if !ok || len(st.puts) != 1 {
|
|
t.Fatalf("entries = %v, want one under g14.nox", st.puts)
|
|
}
|
|
var e map[string]string
|
|
json.Unmarshal(raw, &e)
|
|
if e["node"] != "g14" || e["account"] != "nox" || e["kind"] != "machine" || e["at"] != "2026-10-16T12:00:00Z" {
|
|
t.Errorf("entry = %v", e)
|
|
}
|
|
if !strings.HasPrefix(e["public"], "ssh-ed25519 ") || !strings.HasSuffix(e["public"], " nox@g14 (mesh)") {
|
|
t.Errorf("public = %q", e["public"])
|
|
}
|
|
if !strings.HasPrefix(e["fingerprint"], "SHA256:") {
|
|
t.Errorf("fingerprint = %q", e["fingerprint"])
|
|
}
|
|
if strings.Contains(strings.ToUpper(string(raw)), "PRIVATE") {
|
|
t.Errorf("the entry carries private material: %s", raw)
|
|
}
|
|
cmd := strings.Join((*calls)[0], " ")
|
|
for _, want := range []string{"-t ed25519", "-N ", "-C nox@g14 (mesh)", filepath.Join(home, ".ssh", "mesh_ed25519")} {
|
|
if !strings.Contains(cmd, want) {
|
|
t.Errorf("ssh-keygen call %q lacks %q", cmd, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestKeeperKeepsAnExistingKeyByteForByte(t *testing.T) {
|
|
home := t.TempDir()
|
|
os.MkdirAll(filepath.Join(home, ".ssh"), 0o700)
|
|
priv, pub := filepath.Join(home, ".ssh", "mesh_ed25519"), filepath.Join(home, ".ssh", "mesh_ed25519.pub")
|
|
os.WriteFile(priv, []byte("mine"), 0o600)
|
|
os.WriteFile(pub, []byte(ed25519Line(9, "nox@g14 (mesh)")+"\n"), 0o644)
|
|
st, calls := &fakeState{}, &[][]string{}
|
|
k := newKeeper(t, home, st, calls)
|
|
if err := k.Once(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(*calls) != 0 {
|
|
t.Errorf("ssh-keygen ran over an existing key: %v", *calls)
|
|
}
|
|
if b, _ := os.ReadFile(priv); string(b) != "mine" {
|
|
t.Errorf("private file changed: %q", b)
|
|
}
|
|
if _, ok := st.puts["g14.nox"]; !ok {
|
|
t.Errorf("no entry for the existing key")
|
|
}
|
|
// A second round with nothing changed writes nothing.
|
|
delete(st.puts, "g14.nox")
|
|
k.Once(context.Background())
|
|
if len(st.puts) != 0 {
|
|
t.Errorf("an unchanged key was put again")
|
|
}
|
|
}
|
|
|
|
func TestKeeperNeverTouchesTheOperatorsOwnKeys(t *testing.T) {
|
|
home := t.TempDir()
|
|
os.MkdirAll(filepath.Join(home, ".ssh"), 0o700)
|
|
own := filepath.Join(home, ".ssh", "id_ed25519")
|
|
os.WriteFile(own, []byte("operator"), 0o600)
|
|
st, calls := &fakeState{}, &[][]string{}
|
|
newKeeper(t, home, st, calls).Once(context.Background())
|
|
if b, _ := os.ReadFile(own); string(b) != "operator" {
|
|
t.Errorf("the operator's key changed")
|
|
}
|
|
}
|