Files
felhom-agent/internal/authz/doc.go
T
admin f0fee7e193 feat(authz): operator signed-op verifier + durable nonce store (slice 2, v0.2.0)
internal/authz: production form of the Phase-4 SSHSIG signing primitive.

- Verifier.New/Verify with the LOCKED pipeline (namespace → allow-list by key
  material → crypto over RAW bytes → target → time → nonce LAST); each post-crypto
  stage rejects even with a valid sig; an invalid sig never burns a nonce.
- SSHSIG framing via x/crypto/ssh (no hand-rolled crypto); key-type-agnostic
  (ed25519 / sk-ssh-ed25519 / rsa / ecdsa via pub.Verify). Fixed namespace
  felhom-op-v1. Typed errors. OpBlob (fixed host_id/guest_id tags) + VerifiedOp.
- NonceStore: MemoryNonceStore + durable crash-safe FileNonceStore (fsync'd append
  log, replay-on-open, compaction, expiry-only pruning; survives restart).
- config.AuthzConfig (nonce path + pinned operational/recovery signer keys).
- Tests (14): real ssh-keygen fixture, per-stage rejection, nonce-not-burned,
  replay, persistence-across-restart, synthetic sk, byte-exactness.

Dep: golang.org/x/crypto v0.52.0 (declares go 1.25 — the Phase-4 doc's "Go 1.24.4 /
x/crypto v0.52.0" pairing doesn't build; build server upgraded to go1.26.0,
backward-compatible). Version 0.1.0 -> 0.2.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 15:23:02 +02:00

38 lines
2.1 KiB
Go

// Package authz is the control-plane-authorization layer: it verifies
// operator-signed destructive ops before the agent executes them. It is what the
// reconcile loop (slice 4) calls to gate destructive desired-state deltas and
// signed one-shot jobs (03 §4, 04). The signing mechanism is proven (Phase 4,
// 14/14) — this package is its production form: a key-type-agnostic SSHSIG
// verifier, the full anti-replay/authorization pipeline, and a durable,
// crash-safe nonce store.
//
// # Mechanism (LOCKED — do not redesign)
//
// - SSHSIG via golang.org/x/crypto/ssh; no hand-rolled crypto, no raw-Ed25519
// fallback. pub.Verify dispatches on the key's own algorithm, so the same path
// accepts ed25519 / sk-ssh-ed25519 (FIDO2) / rsa / ecdsa — a hardware operator
// key later is a box no-op (Phase 4 §5/§6, doc 04 §7).
// - Fixed namespace felhom-op-v1 (package constant, never caller-supplied).
// - The verifier verifies over the RAW received blob bytes and never
// canonicalizes — the canonical form (sorted-key, whitespace-free JSON) is the
// signer's contract, shared by the hub and the felhom-sign CLI.
//
// # Pipeline order (load-bearing — Verify)
//
// parse armor → namespace → parse pubkey → allow-list (by key MATERIAL, not
// key_id) → crypto verify → parse blob → target → time window → nonce LAST
//
// Each post-crypto stage rejects even with an otherwise-valid signature. The nonce
// is recorded last, so an invalid signature can never consume a nonce. key_id is
// advisory/audit only — authz is the key-material allow-list match.
//
// # Shared-contract dependency (flag for later, not built here)
//
// Signatures only verify if the op-generator (hub) and the felhom-sign CLI produce
// BYTE-IDENTICAL canonical JSON (keys sorted at every level, no insignificant
// whitespace, no trailing newline, UTF-8 — Phase 4 §2). The verifier deliberately
// does NOT re-canonicalize, so a divergence between those two producers surfaces as
// a crypto failure here. A shared canonicalizer that both import would be the right
// home for that contract; it is out of scope for this slice.
package authz