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>
4.1 KiB
REPORT — slice 10B (agent half): operator-signed destructive completion (v0.16.0) (2026-06-10)
Overwrite-latest report. Cumulative history: CHANGELOG.md.
What was implemented
The security centerpiece: a destructive op runs ONLY on a verified, operator-signed authorization.
Decision (a): offline operator key + signing CLI, hardware-key-ready. The signing key is NOT in
the hub and NOT in the agent. Concrete consumer: this closes the 8C data-bearing-wipe
pending_signature gap. Pairs with hub v0.10.0.
The flow (end-to-end)
8C format of a data-bearing device → agent refuses pending_signature + surfaces the bound op
(durable id + host) → operator signs offline (felhom-opsign) → uploads to the hub jobs queue →
agent's signed-jobs runner verifies + executes the wipe (re-resolve durable id + re-inspect
8C → mkfs) → clears the job.
cmd/felhom-opsign (new) — the offline signing CLI
- Builds the canonical
OpBlobby reusingauthz.CanonicalBlob(the exact bytes the verifier authenticates over — signer/verifier can't drift) and signs withssh-keygen -Y sign -n felhom-op-v1(hardware-ready:sk-/YubiKey work unchanged). Output:{op_blob_b64, sig_armored}; optional--upload. Touches only the operator's key.
The verify-and-execute machinery
internal/signedjobs.Runner— fetches each opaque job → runs the gate (the LOCKED authz pipeline: pinned-key SSHSIG → namespace → allow-list by key MATERIAL → crypto over raw bytes → host target → time window → durable nonce-burn LAST) → on all-pass hands the verified op to anExecutor. Order: verify → burn nonce (durable) → execute → clear. Rejects (forged/replayed/ expired/retargeted/non-pinned) never reach the executor.WipeExecutor(the 8C consumer) — resolve the signed durable id → re-derive + match (anti-retarget) → re-inspect (8C classifier) still-data-bearing →mkfs. A path-only binding, a vanished/changed device, or a non-data-bearing target is refused even with a valid signature.internal/storagedurable identity —DeviceDurableID/ResolveDurableDeviceover the world-readable udev symlinks (byid:wwn/serial,byuuid:fallback) — no privilege, no subprocess.authz.CanonicalBlobpromoted to production.hub.Client.Jobs/CompleteJob+MultiObserver. The 8C 403 now carries apending_op(op + durable id + host) + afelhom-opsignhint.
Pinning / rotation
Operator pubkeys pinned via authz.signers (config, trusted path — NEVER hub-alone), multi-key
(KeyID selects, role-scoped) for backup/rotation without a flag-day. Unchanged verifier wiring; 10B
activates the execute path (the runner is the second EnvelopeObserver, runs on HasSignedOps).
Tests (real crypto, non-hollow — assert the op did/did NOT run)
signedjobsover the real gate+verifier (in-Go minted SSHSIGs): valid → executor runs once + job cleared; replay / non-pinned / expired / retarget / forged / no-signer → rejected, executor never called; malformed cleared.WipeExecutor: valid →mkfs; path-only / durable-mismatch / device-gone / re-inspect-non-data- bearing / not-probed → refused,Formatnot called.storagedurable: wwn-preference, uuid-fallback, path-only+traversal refusal, round-trip, missing (symlink tests gated to Linux).go test ./...green.
Versioning / docs
- Version
0.15.0 → 0.16.0;CHANGELOG.md. Doc 03 §4 (signed path live) + §6 (8C wipe completes) + §9 (10B done) updated.
Out of scope (per the task)
- Other destructive executors (guest_destroy, decommission, restore-overwrite → 10D) reuse the same gate+runner; their executors plug in per-slice. 10B ships the machinery + the storage-wipe.
Pending
- Live validation on the demo: data-bearing wipe →
pending_signature→ sign offline with a real operator key → hub queue → agent verifies + wipes; confirm replay + a non-pinned-key signature are rejected. (Also validates the Linux-only durable-device tests + ssh-keygen interop.)