Files
felhom-agent/internal/signedjobs/wipe_test.go
T
admin 588fed2aa9 slice 10B: operator-signed destructive completion (offline key + signing CLI) (v0.16.0)
A destructive op runs ONLY on a pinned-key-verified, nonce-fresh, in-window,
host-bound, durable-id-bound operator signature. New cmd/felhom-opsign signs
canonical OpBlobs offline via ssh-keygen -Y sign (hardware-ready); the signing
key is never in the hub or agent. New internal/signedjobs runner verifies each
queued blob through the gate and only on all-pass runs the WipeExecutor, which
re-resolves the DURABLE device id + re-inspects (8C) before mkfs — closing the
8C data-bearing-wipe pending_signature gap. New storage durable-device
resolution; authz.CanonicalBlob promoted to production. Real-crypto tests assert
valid executes and forged/replay/expired/retarget/non-pinned are rejected
(executor never called).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 20:14:16 +02:00

138 lines
5.5 KiB
Go

package signedjobs
import (
"context"
"encoding/json"
"errors"
"testing"
"gitea.dooplex.hu/admin/felhom-agent/internal/storage"
)
// fakeWipeOps records Format calls and returns a configurable InspectDevice probe.
type fakeWipeOps struct {
probe storage.DeviceProbe
inspectErr error
formatCalls []string
}
func (f *fakeWipeOps) InspectDevice(_ context.Context, device string) (storage.DeviceProbe, error) {
p := f.probe
p.Device = device
return p, f.inspectErr
}
func (f *fakeWipeOps) Format(_ context.Context, device, _ string) error {
f.formatCalls = append(f.formatCalls, device)
return nil
}
// newWipeExec builds a WipeExecutor with injected resolve/derive (so durable resolution is
// deterministic in a unit test, no real /dev needed).
func newWipeExec(ops WipeOps, resolve func(string) (string, error), derive func(string) (string, error)) *WipeExecutor {
w := NewWipeExecutor(ops, quiet())
w.resolve = resolve
w.derive = derive
return w
}
const durable = "byid:wwn-0xtest"
func params(t *testing.T, durableID, fstype string) json.RawMessage {
t.Helper()
b, _ := json.Marshal(map[string]string{"durable_id": durableID, "fstype": fstype})
return b
}
// VALID: durable resolves, re-derives to the same id, device is data-bearing → Format runs.
func TestWipe_ValidExecutes(t *testing.T) {
ops := &fakeWipeOps{probe: storage.DeviceProbe{Probed: true, HasFilesystem: true, FSType: "ext4"}}
w := newWipeExec(ops,
func(id string) (string, error) { return "/dev/sdb", nil },
func(dev string) (string, error) { return durable, nil })
if err := w.Execute(context.Background(), "storage_wipe", params(t, durable, "ext4")); err != nil {
t.Fatalf("valid wipe: %v", err)
}
if len(ops.formatCalls) != 1 || ops.formatCalls[0] != "/dev/sdb" {
t.Fatalf("Format calls = %v, want one on /dev/sdb", ops.formatCalls)
}
}
// PATH-ONLY (no durable_id) → refused, Format NOT called.
func TestWipe_PathOnlyRefused(t *testing.T) {
ops := &fakeWipeOps{probe: storage.DeviceProbe{Probed: true, HasFilesystem: true}}
w := newWipeExec(ops, func(string) (string, error) { return "/dev/sdb", nil }, func(string) (string, error) { return durable, nil })
// params carry a raw device path, NOT a durable id.
p, _ := json.Marshal(map[string]string{"device": "/dev/sdb", "fstype": "ext4"})
if err := w.Execute(context.Background(), "storage_wipe", p); err == nil {
t.Fatal("path-only wipe must be refused")
}
if len(ops.formatCalls) != 0 {
t.Errorf("Format was called on a path-only binding: %v", ops.formatCalls)
}
}
// DURABLE MISMATCH: the resolved device re-derives to a DIFFERENT id → refused (anti-retarget).
func TestWipe_DurableMismatchRefused(t *testing.T) {
ops := &fakeWipeOps{probe: storage.DeviceProbe{Probed: true, HasFilesystem: true}}
w := newWipeExec(ops,
func(string) (string, error) { return "/dev/sdb", nil },
func(string) (string, error) { return "byid:wwn-0xDIFFERENT", nil }) // re-derive disagrees
if err := w.Execute(context.Background(), "storage_wipe", params(t, durable, "ext4")); err == nil {
t.Fatal("a durable-id mismatch must be refused")
}
if len(ops.formatCalls) != 0 {
t.Errorf("Format was called despite a durable-id mismatch: %v", ops.formatCalls)
}
}
// DEVICE GONE: the durable id no longer resolves → refused (device removed/replaced).
func TestWipe_ResolveFailureRefused(t *testing.T) {
ops := &fakeWipeOps{probe: storage.DeviceProbe{Probed: true, HasFilesystem: true}}
w := newWipeExec(ops,
func(string) (string, error) { return "", errors.New("no such by-id link") },
func(string) (string, error) { return durable, nil })
if err := w.Execute(context.Background(), "storage_wipe", params(t, durable, "ext4")); err == nil {
t.Fatal("an unresolvable durable id must be refused")
}
if len(ops.formatCalls) != 0 {
t.Errorf("Format was called on an unresolvable device: %v", ops.formatCalls)
}
}
// RE-INSPECT: the resolved device is no longer data-bearing (target changed) → refused.
func TestWipe_ReinspectNonDataBearingRefused(t *testing.T) {
ops := &fakeWipeOps{probe: storage.DeviceProbe{Probed: true /* blank: no fs/parts */}}
w := newWipeExec(ops,
func(string) (string, error) { return "/dev/sdb", nil },
func(string) (string, error) { return durable, nil })
if err := w.Execute(context.Background(), "storage_wipe", params(t, durable, "ext4")); err == nil {
t.Fatal("a device that is no longer data-bearing must be refused (target changed)")
}
if len(ops.formatCalls) != 0 {
t.Errorf("Format was called on a non-target device: %v", ops.formatCalls)
}
}
// RE-INSPECT: the device did not probe cleanly → refused (fail-safe).
func TestWipe_NotProbedRefused(t *testing.T) {
ops := &fakeWipeOps{probe: storage.DeviceProbe{Probed: false}}
w := newWipeExec(ops,
func(string) (string, error) { return "/dev/sdb", nil },
func(string) (string, error) { return durable, nil })
if err := w.Execute(context.Background(), "storage_wipe", params(t, durable, "ext4")); err == nil {
t.Fatal("a device that did not probe cleanly must be refused")
}
if len(ops.formatCalls) != 0 {
t.Errorf("Format was called on an unprobed device: %v", ops.formatCalls)
}
}
// A non-wipe op → ErrNoExecutor (left for the owning slice; Format not called).
func TestWipe_OtherOpIsNoExecutor(t *testing.T) {
ops := &fakeWipeOps{}
w := newWipeExec(ops, func(string) (string, error) { return "/dev/sdb", nil }, func(string) (string, error) { return durable, nil })
if err := w.Execute(context.Background(), "guest_destroy", params(t, durable, "ext4")); !errors.Is(err, ErrNoExecutor) {
t.Fatalf("guest_destroy err = %v, want ErrNoExecutor", err)
}
}