f0fee7e193
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>
38 lines
2.1 KiB
Go
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
|