# felhom-agent — latest task report > This file holds the report for the **most recent** change, fully overwritten each task. > Cumulative history lives in [CHANGELOG.md](CHANGELOG.md). ## Task: `authz` signed-op verifier (slice 2) — v0.2.0 Turned the Phase-4 reference `VerifySignedOp` into a production package (`internal/authz`): a key-type-agnostic SSHSIG verifier for operator-signed destructive ops, the full anti-replay/authorization pipeline, and a durable, crash-safe nonce store. This is what slice 4 (reconcile) calls to gate destructive desired-state deltas. Pushed to `main`. Build/vet/test green locally (Go 1.26) and on the build server. ### Public surface (`internal/authz`) - **`Verifier`** — `New(signers []AllowedSigner, store NonceStore, hostID string) *Verifier`; `Verify(blob, sigArmored []byte) (*VerifiedOp, error)`. Optional `ClockSkew` (default 2m, not-yet-valid only) and `Logger` (advisory key_id-mismatch warning). - **`OpBlob`** — canonical signed object; `Target{HostID,GuestID}` with corrected `host_id`/`guest_id` json tags; `Params json.RawMessage`, `Nonce`, `IssuedAt`, `ExpiresAt`, `KeyID`. - **`VerifiedOp`** — `Op, HostID, GuestID, Params, Nonce, IssuedAt, ExpiresAt, KeyID (advisory), Signer (matched), KeyIDMatchesSigner`. - **`AllowedSigner`** + `NewAllowedSigner(keyID, role, authorizedKeyLine)`; roles `RoleOperational` / `RoleRecovery` (doc 04 two-key model; role-scoping enforced by the caller). - **`NonceStore`** interface + `MemoryNonceStore` (tests) and **`FileNonceStore`** (durable). - **Typed errors**: `ErrMalformed, ErrNamespace, ErrUnknownSigner, ErrBadSignature, ErrTarget, ErrExpired, ErrNotYetValid, ErrReplay` (errors.Is-friendly). - **Config**: `config.AuthzConfig` (nonce-store path + pinned `Signers`). ### Locked pipeline (order load-bearing) `parse armor → namespace (fixed felhom-op-v1) → parse pubkey → allow-list by key MATERIAL (not key_id) → crypto verify over RAW received bytes → parse blob → target (host strict, guest surfaced) → time window → nonce recorded LAST`. Each post-crypto stage rejects even with a valid signature; an invalid signature can never consume a nonce. ### Durable nonce store — mechanism & guarantee fsync'd append-only JSONL log + in-memory index (replayed on open) + periodic compaction. - **Crash-safe**: a nonce is written and `fsync`'d before `SeenOrRecord` returns `false`, so the caller acts only *after* the durable record. A crash between verify and execute drops the op (fail-safe) and never enables a replay. I/O failure → returns seen=true (op not executed). - **Survives restart**: the log is replayed into the index on `OpenFileNonceStore`. - **Pruning**: expired nonces dropped only at compaction (never before exp) — and an expired op is rejected by the time check before the nonce check, so pruning is housekeeping, not a hole. - **Concurrency-safe**: single mutex over file handle + index. ### OPEN choices - **Clock skew**: 2-minute tolerance on *not-yet-valid* only; expiry not extended (window stays an honest bound). - **Durable mechanism**: fsync'd append log + compaction (simple, honest, no embedded-KV dep). - **Fixtures**: committed real `ssh-keygen -Y sign` vector (hermetic + proves OpenSSH interop) + in-Go minting for rejection cases; the sk case is synthetic (spec-faithful, no hardware). - **Package name**: `authz` (control-plane-authorization layer, matches doc 04). ### Test matrix (all pass — 14 tests) Real ssh-keygen fixture · happy path · per-stage rejection {namespace, unknown-signer, tampered, retargeted-host, expired, not-yet-valid, replay} · **invalid-sig-does-NOT-burn-nonce** (then the valid op with that nonce still succeeds) · replay-rejected-across-restart (durable store) · key-type-agnostic synthetic **sk-ssh-ed25519** · byte-exactness (re-serialized blob fails crypto). ### Corrections to the Phase-4 §7 reference (for production) - `Target` needed `host_id`/`guest_id` json tags — fixed. - **The doc's "Go 1.24.4 / x/crypto v0.52.0" does not hold**: x/crypto v0.52.0 declares `go 1.25.0` and won't build on Go 1.24. Resolved by upgrading the build server to **go1.26.0** (backward-compatible — felhom-controller/hub build unchanged; distro Go package left intact, upstream Go fronted on PATH). - Free function → constructed `Verifier`; returns full `VerifiedOp`; typed errors; clock-skew; durable nonce store (the net-new engineering). - **Shared-contract flag (not built)**: the hub and `felhom-sign` CLI must produce byte-identical canonical JSON or signatures won't verify; a shared canonicalizer both import is the right home. ### Verification - `go build/vet/test` green locally (go1.26.0) and on the build server (upgraded to go1.26.0). - Real OpenSSH `ssh-keygen` (OpenSSH 10.0p2) minted the committed fixture and self-verified it before commit. ### Repo state - Branch: `main` only. Dep: `golang.org/x/crypto v0.52.0` (+ `x/sys` indirect); `go 1.25.0`.