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>
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
package authz
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Target binds an op to a specific box (and optionally a guest) — the anti-retarget
|
||||
// field. The §7 reference omitted the json tags; production needs them so the
|
||||
// signed canonical bytes decode correctly.
|
||||
type Target struct {
|
||||
HostID string `json:"host_id"`
|
||||
GuestID string `json:"guest_id"`
|
||||
}
|
||||
|
||||
// OpBlob is the canonical signed object (phase4 §2). The signature covers the
|
||||
// EXACT bytes of this object's canonical JSON (keys sorted at every level, no
|
||||
// insignificant whitespace, no trailing newline, UTF-8) — produced by the
|
||||
// operator CLI / hub, verified here over the raw received bytes.
|
||||
type OpBlob struct {
|
||||
Op string `json:"op"`
|
||||
Target Target `json:"target"`
|
||||
Params json.RawMessage `json:"params"`
|
||||
Nonce string `json:"nonce"`
|
||||
IssuedAt time.Time `json:"issued_at"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
KeyID string `json:"key_id"`
|
||||
}
|
||||
|
||||
// VerifiedOp is the authenticated, parsed op returned on success — everything the
|
||||
// reconcile layer (slice 4) needs to route and execute, not just the op string.
|
||||
type VerifiedOp struct {
|
||||
Op string // the operation, e.g. "guest_destroy"
|
||||
HostID string // target host (== this agent's host)
|
||||
// GuestID is non-empty for a guest-scoped op; the caller routes by it. "" =
|
||||
// host-scoped op. The verifier does NOT need to know all guest ids.
|
||||
GuestID string
|
||||
Params json.RawMessage
|
||||
Nonce string
|
||||
IssuedAt time.Time
|
||||
ExpiresAt time.Time
|
||||
|
||||
// KeyID is the blob's self-declared key id — ADVISORY / audit only, never an
|
||||
// authz input. Authz is the key-material allow-list match (Signer below).
|
||||
KeyID string
|
||||
// Signer is the allow-listed key whose material matched the signature.
|
||||
Signer AllowedSigner
|
||||
// KeyIDMatchesSigner is false when the blob's advisory KeyID disagrees with
|
||||
// the matched signer's id (a benign audit signal, not a rejection).
|
||||
KeyIDMatchesSigner bool
|
||||
}
|
||||
Reference in New Issue
Block a user