Files
felhom.eu/hub/internal/offsitekeys/offsitekeys_test.go
T
admin f417cdede1
gates / gates (push) Successful in 29s
hub v0.127.0: off-site key registrar (box never gets the storage password), password sealed at rest, daily key check, clean-up window (shipped off) — decisions 68-69, R-820/R-821/R-822
Part A evidence (migration spike, sftp-written repo through the pinned rclone key) and the hub
red-proofs under documentation/audits/offsite-lock-build-2026-10-03/. Manifest bump follows after
the image is built and Secret/offsite-secret-key exists.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-10-03 16:57:04 +02:00

217 lines
7.1 KiB
Go

package offsitekeys
import (
"context"
"crypto/ed25519"
"crypto/rand"
"errors"
"strings"
"testing"
"golang.org/x/crypto/ssh"
)
// fakeFS emulates the provider's restricted shell as MEASURED 2026-10-03: `dd of=` takes stdin, `mv`
// overwrites, `cat`/`ls` of a missing path exit non-zero, `test` does not exist.
type fakeFS struct {
files map[string]string
dirs map[string]bool
cmds []string
}
func newFS() *fakeFS { return &fakeFS{files: map[string]string{}, dirs: map[string]bool{".ssh": true}} }
func (f *fakeFS) Run(_ context.Context, cmd string, stdin []byte) ([]byte, error) {
f.cmds = append(f.cmds, cmd)
a := strings.Fields(cmd)
miss := errors.New("exit status 1")
switch {
case a[0] == "ls" && a[1] == "-d":
if f.dirs[a[2]] {
return []byte(a[2] + "\n"), nil
}
return nil, miss
case a[0] == "ls":
if _, ok := f.files[a[1]]; ok {
return []byte(a[1]), nil
}
return nil, miss
case a[0] == "cat":
if v, ok := f.files[a[1]]; ok {
return []byte(v), nil
}
return nil, miss
case a[0] == "mkdir":
f.dirs[a[1]] = true
return nil, nil
case a[0] == "chmod":
return nil, nil
case strings.HasPrefix(a[0], "dd") && strings.HasPrefix(a[1], "of="):
f.files[strings.TrimPrefix(a[1], "of=")] = string(stdin)
return nil, nil
case a[0] == "mv":
if v, ok := f.files[a[1]]; ok {
f.files[a[2]] = v
delete(f.files, a[1])
return nil, nil
}
if f.dirs[a[1]] {
f.dirs[a[2]] = true
delete(f.dirs, a[1])
return nil, nil
}
return nil, miss
case a[0] == "rm":
return nil, errors.New("the registrar must never delete")
}
return nil, errors.New("Command not found")
}
func (f *fakeFS) Close() error { return nil }
type fakeDialer struct{ fs *fakeFS }
func (d fakeDialer) Dial(context.Context, Target, string) (Shell, error) { return d.fs, nil }
var tgt = Target{Host: "u1-sub4.example", User: "u1-sub4", Port: 23, RepoPath: "/home/felhom-repo", Fingerprint: "SHA256:host"}
func newKey(t *testing.T) (pub, fp string) {
t.Helper()
k, _, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
pk, _ := ssh.NewPublicKey(k)
return strings.TrimSpace(string(ssh.MarshalAuthorizedKey(pk))) + " box", ssh.FingerprintSHA256(pk)
}
// The MIGRATION shape: a box whose key predates the pin (an unpinned line, exactly as ssh-copy-id left
// it) registers the SAME key → it comes back pinned and the unpinned line is gone. Then the audit is clean.
func TestInstall_MigratesUnpinnedKeyAndAuditGoesClean(t *testing.T) {
fs := newFS()
pub, fp := newKey(t)
other, _ := newKey(t)
fs.files[".ssh/authorized_keys"] = pub + "\n" + other + "\n"
r := &Registrar{Dialer: fakeDialer{fs}}
before, err := r.Audit(context.Background(), tgt, "pw", false)
if err != nil || len(before.Findings) != 2 {
t.Fatalf("before: %+v %v — want 2 unpinned findings (the decoy must be seen)", before, err)
}
res, err := r.Install(context.Background(), tgt, "pw", pub)
if err != nil || res.Fingerprint != fp || res.RemovedUnpinned != 2 {
t.Fatalf("install = %+v, %v", res, err)
}
got := fs.files[".ssh/authorized_keys"]
if !strings.HasPrefix(got, PinnedPrefix(tgt.RepoPath)) || strings.Count(got, "\n") != 1 {
t.Fatalf("file after install:\n%s", got)
}
after, _ := r.Audit(context.Background(), tgt, "pw", false)
if len(after.Findings) != 0 || after.Pinned != 1 {
t.Fatalf("after: %+v", after)
}
for _, c := range fs.cmds {
if strings.HasPrefix(c, "rm") {
t.Fatalf("registrar issued a delete: %q", c)
}
}
}
// Rotation: new key installed beside the old pinned one; Confirm leaves only the new one.
func TestInstallThenConfirm_Rotation(t *testing.T) {
fs := newFS()
r := &Registrar{Dialer: fakeDialer{fs}}
oldPub, oldFP := newKey(t)
newPub, newFP := newKey(t)
if _, err := r.Install(context.Background(), tgt, "pw", oldPub); err != nil {
t.Fatal(err)
}
if _, err := r.Install(context.Background(), tgt, "pw", newPub); err != nil {
t.Fatal(err)
}
lines := ParseLines(fs.files[".ssh/authorized_keys"], tgt.RepoPath)
if len(lines) != 2 || !lines[0].Pinned || !lines[1].Pinned {
t.Fatalf("both keys must be pinned until confirm: %+v", lines)
}
if _, err := r.Confirm(context.Background(), tgt, "pw", "SHA256:not-installed"); err == nil {
t.Fatal("confirming an absent key must refuse")
}
n, err := r.Confirm(context.Background(), tgt, "pw", newFP)
if err != nil || n != 1 {
t.Fatalf("confirm = %d, %v", n, err)
}
lines = ParseLines(fs.files[".ssh/authorized_keys"], tgt.RepoPath)
if len(lines) != 1 || lines[0].Fingerprint != newFP || lines[0].Fingerprint == oldFP {
t.Fatalf("after confirm: %+v", lines)
}
}
// The window (decision 68): the deleting line goes FIRST (first match wins — measured), the audit
// tolerates it only while a window is open, and closing removes it.
func TestWindow_PrependAuditClose(t *testing.T) {
fs := newFS()
r := &Registrar{Dialer: fakeDialer{fs}}
pub, fp := newKey(t)
if err := r.OpenWindow(context.Background(), tgt, "pw", fp); err == nil {
t.Fatal("a window for a key that is not installed must refuse")
}
if _, err := r.Install(context.Background(), tgt, "pw", pub); err != nil {
t.Fatal(err)
}
if err := r.OpenWindow(context.Background(), tgt, "pw", fp); err != nil {
t.Fatal(err)
}
lines := ParseLines(fs.files[".ssh/authorized_keys"], tgt.RepoPath)
if len(lines) != 2 || !lines[0].Window || !lines[1].Pinned || lines[0].Fingerprint != fp {
t.Fatalf("window line must be first: %+v", lines)
}
if a, _ := r.Audit(context.Background(), tgt, "pw", true); len(a.Findings) != 0 {
t.Fatalf("open window flagged: %+v", a)
}
if a, _ := r.Audit(context.Background(), tgt, "pw", false); len(a.Findings) != 1 || a.Findings[0].Kind != "window" {
t.Fatalf("a window line with no open window must alarm: %+v", a)
}
if err := r.CloseWindow(context.Background(), tgt, "pw"); err != nil {
t.Fatal(err)
}
if a, _ := r.Audit(context.Background(), tgt, "pw", false); len(a.Findings) != 0 || a.Pinned != 1 {
t.Fatalf("after close: %+v", a)
}
}
// Move-aside renames, never deletes, and never reuses a name.
func TestMoveAside_RenamesNeverDeletes(t *testing.T) {
fs := newFS()
fs.dirs["/home/felhom-repo"] = true
fs.dirs["/home/felhom-repo.orphaned-20261003"] = true
r := &Registrar{Dialer: fakeDialer{fs}}
to, err := r.MoveAside(context.Background(), tgt, "pw", "20261003")
if err != nil || to != "/home/felhom-repo.orphaned-20261003-2" {
t.Fatalf("move-aside = %q, %v", to, err)
}
if fs.dirs["/home/felhom-repo"] || !fs.dirs["/home/felhom-repo.orphaned-20261003"] {
t.Fatalf("dirs after: %v", fs.dirs)
}
}
func TestTargetWithoutFingerprint_Refused(t *testing.T) {
r := &Registrar{Dialer: fakeDialer{newFS()}}
pub, _ := newKey(t)
nt := tgt
nt.Fingerprint = ""
if _, err := r.Install(context.Background(), nt, "pw", pub); err == nil {
t.Fatal("no host fingerprint must refuse (no blind TOFU)")
}
}
func TestKeyFingerprint_RefusesOptionsAndJunk(t *testing.T) {
pub, _ := newKey(t)
for _, bad := range []string{"", "not a key", `command="sh" ` + pub, pub + "\n" + pub} {
if _, _, err := KeyFingerprint(bad); err == nil {
t.Fatalf("accepted %q", bad)
}
}
if _, _, err := KeyFingerprint(pub); err != nil {
t.Fatal(err)
}
}