Files
zeekayandHanzo Dev a20ace0dd0 a request names the channel it is for, and is honoured only there
Key agreement gives two endpoints a private channel. It does not tell
either of them who is on the far end, so a signature on a request proves
who wrote it and not who they wrote it to. Anyone able to agree keys with
both sides can therefore carry a signed request from one channel to
another and be served on it.

The handshake now derives a second value alongside the session key, under
its own label: a binding that names the channel. Only the two endpoints
that ran that handshake can compute it. The caller signs it into every
envelope; the peer checks it against its own side of the channel the
envelope arrived on. A request written for one channel is inert on any
other — it cannot be re-signed without the caller's key, and a channel
cannot be made to produce a name it did not derive.

It is not optional anywhere it could be skipped:

  - NewSession takes the handshake result, so a session always carries
    the binding derived with its key.
  - envelope.Build requires one, so an envelope addressed to nobody
    cannot be signed; Parse requires one, so it cannot be read.
  - envelope.Verify takes the arriving channel as a parameter, so there
    is no way to verify without saying where the request came from.
  - Both transports supply their own: the ZAP wire its session's, the
    HTTP surface the TLS exporter. A connection with no keys of its own
    has no binding and can carry no envelope.

zapclient loses SkipHandshake and RequireSession. With the binding
required, a client without a session has nothing to sign and cannot
speak, so the dial either agrees keys or fails, and a peer that answers
classical-only when ML-KEM-768 was offered is refused outright rather
than by configuration. VerifyEnvelope goes with them: a verify entry
that checked neither the ledger nor the channel had no callers and no
business existing.

Tests: an attacker that terminates and re-originates both halves of the
handshake, built from the shipping client and server, is refused — both
when it forwards the envelope verbatim and when it rewrites the name to
its own. A caller reaching the KMS directly still gets its secret.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-15 04:49:47 -07:00

641 lines
23 KiB
Go

// Package zapserver exposes the KMS Secret store over luxfi/zap.
//
// Wire format per request: opcode(2 LE) || envelope JSON.
// Response: 1-byte status (0x00 ok, 0x01 not found, 0x02 error,
// 0x03 forbid) || payload.
//
// Opcodes:
//
// 0x0040 OpSecretGet { path, name, env } → { value: base64 } or not-found
// 0x0041 OpSecretPut { path, name, env, value } → { ok: true } (admin only)
// 0x0042 OpSecretList { path, env } → { secrets: [{path,env,name}] }
// 0x0043 OpSecretDelete{ path, name, env } → { ok: true } (admin only)
//
// Auth: every secret-opcode payload is wrapped in a signed Envelope
// (see auth.go). The envelope carries the caller's mnemonic-derived
// service NodeID (ML-DSA-65 scheme), a 48-byte SHAKE256-384 commitment
// to the identity, and an ML-DSA-65 signature over the canonical
// envelope digest. The envelope verifier is pure-function and runs in
// the wire path before any store I/O.
//
// Authorization is consensus-native (see consensus_auth.go). The kmsd
// holds one ConsensusAuthorizer; the authorizer asks consensus "is
// this verified NodeID a member of the current validator authority?
// for writes, additionally the operator authority?". There is no
// CSV ACL, no fallback path. If the authorizer is unconfigured the
// server refuses to boot.
package zapserver
import (
"context"
"encoding/base64"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"sync"
"time"
"github.com/luxfi/keys"
"github.com/luxfi/kms/pkg/envelope"
"github.com/luxfi/kms/pkg/secret"
"github.com/luxfi/kms/pkg/store"
kmszap "github.com/luxfi/kms/pkg/zap"
"github.com/luxfi/log"
"github.com/luxfi/zap"
)
const (
OpSecretGet uint16 = 0x0040
OpSecretPut uint16 = 0x0041
OpSecretList uint16 = 0x0042
OpSecretDelete uint16 = 0x0043
// Threshold key ops. Dispatched to the SignBackend (luxfi/mpc
// t-of-n cluster). Exposed on the HTTP /v1/sdk surface; the KMS
// process never holds full key material.
OpSign uint16 = 0x0050
OpVerify uint16 = 0x0051
)
// status byte values in the response.
const (
statusOK byte = 0x00
statusNotFound byte = 0x01
statusError byte = 0x02
statusForbid byte = 0x03
)
// Server wires a SecretStore onto a ZAP Node. It does not own the node's
// lifecycle — the caller registers the server and starts the node.
type Server struct {
store *store.SecretStore
masterKey []byte
authz ConsensusAuthorizer
verifier *envelope.VerifierWithLedger
// signer is the optional threshold-signing backend for OpSign /
// OpVerify. nil ⇒ the sign/verify ops return a clear "signing not
// configured" (mirrors the fail-open MPC posture of the key
// routes). The KMS never holds full key material — the backend
// delegates to the luxfi/mpc t-of-n cluster.
signer SignBackend
log log.Logger
now func() time.Time
// Per-peer hybrid handshake sessions. Keyed by ZAP NodeID. A peer
// with no entry has not run the application-layer hybrid handshake
// — its requests still flow in the clear (forward-only fallback for
// peers that don't speak the new opcodes). A peer with an entry
// gets every payload AEAD-sealed under the derived session key.
sessions map[string]*kmszap.Session
sessionsMu sync.RWMutex
// localCaps controls what the server advertises in ServerHello.
// Bit 0 = ML-KEM-768 supported. Defaults to CapMLKEM768 in New().
localCaps uint16
}
// Config wires a Server with the required dependencies.
type Config struct {
Store *store.SecretStore
MasterKey []byte
// Authorizer is the consensus-native authorization predicate. The
// kmsd asks this every request: "is the verified envelope identity
// authorized for op on path?". Required; nil refuses to boot.
// See consensus_auth.go.
Authorizer ConsensusAuthorizer
// NonceLedger ledgers (NodeID, Nonce) tuples and rejects duplicates
// within the freshness window. nil → an in-memory ledger is
// constructed with envelope defaults (TTL = MaxClockSkew + 1m).
// Pass an explicit NonceLedger to share state across replicas (e.g.
// a disk-backed impl for HA kmsd) or to tune TTL / GC cadence.
NonceLedger NonceLedger
// Signer is the optional threshold-signing backend for the OpSign /
// OpVerify ops on the /v1/sdk surface. nil ⇒ sign/verify return
// statusError("signing not configured"). Never holds full key
// material; delegates to luxfi/mpc.
Signer SignBackend
// Logger is the luxfi/log Logger. nil falls back to the package
// root logger (log.Root()).
Logger log.Logger
// Now is the wall-clock used for envelope freshness checks. nil
// defaults to time.Now. Tests pin this.
Now func() time.Time
}
// New returns a Server ready to attach to a ZAP node via Register.
// Panics if masterKey is not 32 bytes (caller must unseal first) or if
// the authorizer is nil (the server is fail-closed by construction —
// there is no open mode).
func New(cfg Config) *Server {
if len(cfg.MasterKey) != 32 {
panic("zapserver: master key must be 32 bytes")
}
if cfg.Authorizer == nil {
panic("zapserver: consensus authorizer is required")
}
if cfg.Logger == nil {
cfg.Logger = log.Root()
}
if cfg.Now == nil {
cfg.Now = time.Now
}
if cfg.NonceLedger == nil {
// Boot-time default — every kmsd gets replay defence even if
// the caller forgets to wire one. The package-level constants
// pin the production TTL (MaxClockSkew + 1m) and GC cadence
// (TTL/4 = 90s).
cfg.NonceLedger = NewMemoryNonceLedger(MemoryNonceLedgerConfig{})
}
verifier, err := envelope.NewVerifierWithLedger(envelope.VerifierWithLedgerConfig{
Verifier: keys.VerifyServiceEnvelope,
Ledger: cfg.NonceLedger,
})
if err != nil {
// NewVerifierWithLedger only rejects nil args; we just
// defaulted both. A panic here is a programmer error in this
// file, not a user-config issue.
panic("zapserver: verifier-with-ledger construction failed: " + err.Error())
}
s := &Server{
store: cfg.Store,
masterKey: cfg.MasterKey,
authz: cfg.Authorizer,
verifier: verifier,
signer: cfg.Signer,
log: cfg.Logger,
now: cfg.Now,
sessions: make(map[string]*kmszap.Session),
localCaps: kmszap.CapMLKEM768,
}
s.log.Info("kms.zap consensus-native authorization wired (nonce ledger active)")
return s
}
// Register attaches the Server's handlers to the ZAP Node.
//
// Handlers are registered at both the opcode-specific message type (for
// callers that set the ZAP flags/type field correctly) AND at type 0 (the
// default when callers use zap.Builder without setting flags). Type 0 is a
// universal multiplexer that reads the opcode from the first 2 bytes of the
// payload and dispatches internally.
func (s *Server) Register(n *zap.Node) {
// Per-opcode handlers. Each runs envelope verification +
// consensus authorization in wrap() before dispatching to the
// inner handler.
n.Handle(OpSecretGet, s.wrap(OpSecretGet, s.handleGet))
n.Handle(OpSecretPut, s.wrap(OpSecretPut, s.handlePut))
n.Handle(OpSecretList, s.wrap(OpSecretList, s.handleList))
n.Handle(OpSecretDelete, s.wrap(OpSecretDelete, s.handleDelete))
// Application-layer hybrid handshake. Distinct from the secret
// opcodes so a session is established before any get/put runs.
n.Handle(kmszap.OpClientHello, s.handleHandshake)
// Universal handler at type 0: reads opcode from body, dispatches
// through the same verify→authorize→dispatch path so envelope
// verification and consensus authorization run identically to the
// per-opcode case. The op→handler routing is s.dispatch — the one
// router shared with the HTTP /v1/sdk transport.
n.Handle(0, func(ctx context.Context, from string, msg *zap.Message) (*zap.Message, error) {
raw := msg.Root().Bytes(0)
if raw == nil {
// Fallback: caller wrote raw bytes via Builder.WriteBytes without
// an enclosing Object — strip the 16-byte ZAP frame header.
b := msg.Bytes()
if len(b) > zap.HeaderSize {
raw = b[zap.HeaderSize:]
}
}
if len(raw) < 2 {
return respond(statusError, errJSON("empty payload")), nil
}
op := binary.LittleEndian.Uint16(raw[:2])
payload := raw[2:]
// Hybrid handshake on the universal mux path too.
if op == kmszap.OpClientHello {
return s.respondHandshake(from, payload), nil
}
// Every op below carries or returns secret material, and each is
// authorized by an envelope that names a channel. A peer without
// a session has neither, so there is nothing to open and nothing
// to check it against.
sess := s.session(from)
if sess == nil {
return respond(statusForbid, errJSON("no session")), nil
}
pt, err := sess.Open(kmszap.DirClientToServer, payload)
if err != nil {
s.log.Warn("kms.zap session open failed", "from", from, "op", op, "err", err)
return respond(statusError, errJSON("session decrypt failed")), nil
}
payload = pt
ident, innerReq, decisionErr := s.verifyAndAuthorize(ctx, payload, op, sess.Bind())
if decisionErr != nil {
s.log.Info("kms.zap authz",
"decision", "forbid",
"op", Op(op).String(),
"from", from,
"reason", decisionErr.Error(),
)
return s.respondMaybeSealed(from, statusForbid, errJSON(decisionErr.Error())), nil
}
status, body, err := s.dispatch(ctx, ident, op, innerReq)
if err != nil {
s.log.Warn("kms.zap handler error (mux)", "ident", ident.String(), "op", Op(op).String(), "err", err)
return s.respondMaybeSealed(from, statusError, errJSON(err.Error())), nil
}
return s.respondMaybeSealed(from, status, body), nil
})
}
// dispatch is the single op→handler router shared by every transport
// (the ZAP universal mux and the HTTP /v1/sdk surface). It runs AFTER
// verifyAndAuthorize has proven the caller's identity and authority, so
// it does not re-check auth. Unknown ops return statusError as
// defence-in-depth — the authorizer has already rejected any op outside
// the allowed set before dispatch is reached.
func (s *Server) dispatch(ctx context.Context, ident Identity, op uint16, inner []byte) (byte, []byte, error) {
switch op {
case OpSecretGet:
return s.handleGet(ctx, ident, inner)
case OpSecretPut:
return s.handlePut(ctx, ident, inner)
case OpSecretList:
return s.handleList(ctx, ident, inner)
case OpSecretDelete:
return s.handleDelete(ctx, ident, inner)
case OpSign:
return s.handleSign(ctx, ident, inner)
case OpVerify:
return s.handleVerify(ctx, ident, inner)
default:
return statusError, errJSON("unknown opcode"), nil
}
}
// session returns the active hybrid session for a peer, or nil if the
// peer has not run OpClientHello yet (forward-compat fallback).
func (s *Server) session(peerID string) *kmszap.Session {
s.sessionsMu.RLock()
defer s.sessionsMu.RUnlock()
return s.sessions[peerID]
}
// setSession stores a freshly negotiated session, replacing any prior
// entry for the same peer. Replacing is the sane behaviour: a new
// ClientHello means the client wants to rotate.
func (s *Server) setSession(peerID string, sess *kmszap.Session) {
s.sessionsMu.Lock()
s.sessions[peerID] = sess
s.sessionsMu.Unlock()
}
// respondMaybeSealed builds a status||body reply, sealing the body bytes
// under the peer's session key if one exists. Plaintext fallback is a
// design property for peers that never ran the handshake.
func (s *Server) respondMaybeSealed(peerID string, status byte, body []byte) *zap.Message {
if sess := s.session(peerID); sess != nil {
sealed, err := sess.Seal(kmszap.DirServerToClient, body)
if err != nil {
s.log.Error("kms.zap seal failed", "from", peerID, "err", err)
return respond(statusError, errJSON("session seal failed"))
}
return respond(status, sealed)
}
return respond(status, body)
}
// handleHandshake is the per-opcode handler entry for OpClientHello.
func (s *Server) handleHandshake(_ context.Context, from string, msg *zap.Message) (*zap.Message, error) {
raw := msg.Root().Bytes(0)
if raw == nil {
b := msg.Bytes()
if len(b) > zap.HeaderSize {
raw = b[zap.HeaderSize:]
}
}
if len(raw) < 2 {
return respond(statusError, errJSON("empty handshake payload")), nil
}
// Strip the 2-byte opcode prefix written by the client mux.
return s.respondHandshake(from, raw[2:]), nil
}
// respondHandshake runs the server side of the hybrid PQ handshake,
// installs the resulting session, and frames the ServerHello bytes as
// the response payload (status=OK).
//
// On capability fallback (peer cleared bit 0), we emit a one-line WARN
// so operators can spot stragglers, and we still install the session
// (X25519-only — combined-secret length is still 32 bytes).
func (s *Server) respondHandshake(from string, helloBytes []byte) *zap.Message {
replyWire, result, err := kmszap.ServerRespond(s.localCaps, helloBytes)
if err != nil {
s.log.Warn("kms.zap handshake failed", "from", from, "err", err)
return respond(statusError, errJSON(err.Error()))
}
sess, err := kmszap.NewSession(result)
if err != nil {
s.log.Error("kms.zap session init failed", "from", from, "err", err)
return respond(statusError, errJSON(err.Error()))
}
s.setSession(from, sess)
if !result.Hybrid {
s.log.Warn("kms.zap handshake classical-only — peer cleared ML-KEM-768 cap bit",
"from", from, "peerCaps", result.PeerCaps)
} else {
s.log.Info("kms.zap handshake hybrid", "from", from, "alg", "X25519+ML-KEM-768")
}
// ServerHello rides as the body — never AEAD-wrapped, since the key
// only exists after this exchange completes.
return respond(statusOK, replyWire)
}
// handlerFn is the shape of our op handlers after envelope unwrap.
// Identity is the verified service identity carried by the envelope;
// payload is the inner request bytes (Envelope.Req).
type handlerFn func(ctx context.Context, ident Identity, payload []byte) (byte, []byte, error)
// wrap marshals the {status || body} response and logs errors with the
// caller's verified identity. When the peer has an active hybrid
// session, the inbound payload is AEAD-opened and the response body
// is sealed. The envelope is parsed, verified, and authorized before
// the inner handler is invoked; failures short-circuit with
// statusForbid and the reason recorded in the audit log.
func (s *Server) wrap(op uint16, h handlerFn) zap.Handler {
return func(ctx context.Context, from string, msg *zap.Message) (*zap.Message, error) {
// Access body via Root Object. Builder writes the payload at field 0.
raw := msg.Root().Bytes(0)
if raw == nil {
// Fallback: raw frame bytes (for callers that don't use structured
// objects) — strip the 16-byte ZAP header so payload begins with the
// opcode prefix.
b := msg.Bytes()
if len(b) > zap.HeaderSize {
raw = b[zap.HeaderSize:]
}
}
// Opcode was already matched by Node — but we carry it in for tracing.
if len(raw) < 2 {
return respond(statusError, errJSON("empty payload")), nil
}
payload := raw[2:]
sess := s.session(from)
if sess == nil {
return respond(statusForbid, errJSON("no session")), nil
}
pt, err := sess.Open(kmszap.DirClientToServer, payload)
if err != nil {
s.log.Warn("kms.zap session open failed", "from", from, "err", err)
return respond(statusError, errJSON("session decrypt failed")), nil
}
payload = pt
ident, innerReq, decisionErr := s.verifyAndAuthorize(ctx, payload, op, sess.Bind())
if decisionErr != nil {
s.log.Info("kms.zap authz",
"decision", "forbid",
"op", Op(op).String(),
"from", from,
"reason", decisionErr.Error(),
)
return s.respondMaybeSealed(from, statusForbid, errJSON(decisionErr.Error())), nil
}
status, body, err := h(ctx, ident, innerReq)
if err != nil {
s.log.Warn("kms.zap handler error", "ident", ident.String(), "op", Op(op).String(), "err", err)
return s.respondMaybeSealed(from, statusError, errJSON(err.Error())), nil
}
return s.respondMaybeSealed(from, status, body), nil
}
}
// verifyAndAuthorize is the canonical request entry: parse envelope,
// verify signature + freshness, run the consensus authorizer, return
// the verified Identity + the inner request bytes.
//
// On any failure the returned error is a single sentence safe for the
// wire (no internal state). The caller emits the audit row and writes
// statusForbid.
// bind is the binding of the channel the request arrived on. Every
// transport supplies its own; the envelope is honoured only if it names
// the same one.
func (s *Server) verifyAndAuthorize(ctx context.Context, raw []byte, op uint16, bind []byte) (Identity, []byte, error) {
env, err := ParseEnvelope(raw)
if err != nil {
return Identity{}, nil, fmt.Errorf("envelope: %w", err)
}
if env.Op != op {
return Identity{}, nil, fmt.Errorf("envelope: op mismatch (got 0x%04X want 0x%04X)", env.Op, op)
}
ident, err := verifyEnvelopeWithLedger(ctx, s.verifier, env, s.now(), bind)
if err != nil {
// Hide the structured ErrEnvelopeReplay reason behind a wire-
// safe phrase so an off-network attacker cannot probe the
// ledger state. Audit log records the verbose reason; the wire
// only sees "envelope: replay-detected".
if errors.Is(err, ErrEnvelopeReplay) {
return Identity{}, nil, ErrEnvelopeReplay
}
return Identity{}, nil, err
}
path, err := pathFromInnerRequest(op, env.Req)
if err != nil {
return Identity{}, nil, err
}
decision, err := s.authz.Authorize(ctx, ident, path, Op(op))
if err != nil {
return Identity{}, nil, fmt.Errorf("consensus: %s: %w", decision.Reason, err)
}
if !decision.Allow {
return Identity{}, nil, fmt.Errorf("forbidden: %s", decision.Reason)
}
return ident, []byte(env.Req), nil
}
// pathFromInnerRequest extracts the canonical "path" from the opcode's
// inner JSON request shape. Each opcode has a single `path` field;
// missing → "" which the authorizer treats as the root prefix.
func pathFromInnerRequest(op uint16, req json.RawMessage) (string, error) {
if len(req) == 0 {
return "", errors.New("envelope: empty inner request")
}
var anyReq struct {
Path string `json:"path"`
}
if err := json.Unmarshal(req, &anyReq); err != nil {
return "", fmt.Errorf("envelope: inner: %w", err)
}
return anyReq.Path, nil
}
// respond frames { status || json } as a ZAP message.
func respond(status byte, body []byte) *zap.Message {
b := zap.NewBuilder(len(body) + 8)
b.WriteBytes(append([]byte{status}, body...))
raw := b.Finish()
msg, err := zap.Parse(raw)
if err != nil {
// Should never happen — Builder output is always parseable.
return nil
}
return msg
}
func errJSON(msg string) []byte {
b, _ := json.Marshal(map[string]string{"error": msg})
return b
}
// ---- handlers ----
// The four secret-opcode handlers. Each receives the verified Identity
// (from envelope verification + consensus authorization) and the inner
// request JSON bytes (Envelope.Req). Handlers do not re-check auth —
// that is the wrap() path's responsibility.
type getReq struct {
Path string `json:"path"`
Name string `json:"name"`
Env string `json:"env"`
}
type getResp struct {
Value string `json:"value"` // base64 plaintext
}
func (s *Server) handleGet(_ context.Context, ident Identity, payload []byte) (byte, []byte, error) {
var req getReq
if err := json.Unmarshal(payload, &req); err != nil {
return statusError, errJSON(err.Error()), nil
}
sec, err := s.store.Get(req.Path, req.Name, req.Env)
if errors.Is(err, store.ErrSecretNotFound) {
return statusNotFound, errJSON("not found"), nil
}
if err != nil {
return statusError, nil, err
}
pt, err := store.Open(s.masterKey, sec)
if err != nil {
return statusError, nil, err
}
defer zero(pt)
b, _ := json.Marshal(getResp{Value: base64.StdEncoding.EncodeToString(pt)})
s.log.Debug("kms.zap get", "ident", ident.String(), "path", req.Path, "name", req.Name, "env", req.Env)
return statusOK, b, nil
}
type putReq struct {
Path string `json:"path"`
Name string `json:"name"`
Env string `json:"env"`
Value string `json:"value"` // base64 plaintext
}
func (s *Server) handlePut(_ context.Context, ident Identity, payload []byte) (byte, []byte, error) {
var req putReq
if err := json.Unmarshal(payload, &req); err != nil {
return statusError, errJSON(err.Error()), nil
}
pt, err := base64.StdEncoding.DecodeString(req.Value)
if err != nil {
return statusError, errJSON("bad base64"), nil
}
defer zero(pt)
sec, err := store.Seal(s.masterKey, req.Path, req.Name, req.Env, pt)
if err != nil {
return statusError, nil, err
}
if err := s.store.Put(sec); err != nil {
return statusError, nil, err
}
s.log.Info("kms.zap put", "ident", ident.String(), "path", req.Path, "name", req.Name, "env", req.Env)
b, _ := json.Marshal(map[string]bool{"ok": true})
return statusOK, b, nil
}
type listReq struct {
Path string `json:"path"`
Env string `json:"env"`
}
type listResp struct {
// Secrets carries each record's full (path, env, name) coordinate, and it is
// the ONLY answer this op gives. A bare name cannot say WHERE a record
// lives, so a client that listed a subtree and then re-joined those names at
// the path it queried would look up coordinates that do not exist — the list
// would name a secret the matching get could not fetch. The wire carries
// exactly what a get needs, so list-then-get closes by construction.
Secrets []secret.Ref `json:"secrets"`
// Truncated is true when the match set exceeded the store's row cap and the
// answer is a bounded prefix — the caller should narrow with path/env.
Truncated bool `json:"truncated,omitempty"`
}
// handleList answers OpSecretList from the same store.Find the HTTP face uses,
// so the two framings of this store can never disagree about what it holds. An
// omitted path means the whole store; an omitted env means every environment
// (see secret.Query) — neither silently narrows the answer.
func (s *Server) handleList(_ context.Context, ident Identity, payload []byte) (byte, []byte, error) {
var req listReq
if err := json.Unmarshal(payload, &req); err != nil {
return statusError, errJSON(err.Error()), nil
}
refs, truncated, err := s.store.Find(secret.Query{Path: req.Path, Env: req.Env})
if err != nil {
return statusError, nil, err
}
s.log.Debug("kms.zap list", "ident", ident.String(), "path", req.Path, "env", req.Env)
b, _ := json.Marshal(listResp{Secrets: refs, Truncated: truncated})
return statusOK, b, nil
}
type delReq struct {
Path string `json:"path"`
Name string `json:"name"`
Env string `json:"env"`
}
func (s *Server) handleDelete(_ context.Context, ident Identity, payload []byte) (byte, []byte, error) {
var req delReq
if err := json.Unmarshal(payload, &req); err != nil {
return statusError, errJSON(err.Error()), nil
}
if err := s.store.Delete(req.Path, req.Name, req.Env); err != nil {
if errors.Is(err, store.ErrSecretNotFound) {
return statusNotFound, errJSON("not found"), nil
}
return statusError, nil, err
}
s.log.Info("kms.zap delete", "ident", ident.String(), "path", req.Path, "name", req.Name, "env", req.Env)
b, _ := json.Marshal(map[string]bool{"ok": true})
return statusOK, b, nil
}
// zero wipes a byte slice (best effort — caller must still avoid copies).
func zero(b []byte) {
for i := range b {
b[i] = 0
}
}
// BuildRequest is a helper for clients: frames opcode(2 LE) || JSON payload.
// Returns the raw bytes ready for zap.NewBuilder.WriteBytes.
func BuildRequest(op uint16, payload any) ([]byte, error) {
data, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("marshal: %w", err)
}
out := make([]byte, 2+len(data))
binary.LittleEndian.PutUint16(out[0:2], op)
copy(out[2:], data)
return out, nil
}
// ParseResponse extracts the status byte and JSON body from a reply message.
func ParseResponse(raw []byte) (status byte, body []byte, err error) {
if len(raw) < 1 {
return 0, nil, fmt.Errorf("empty response")
}
return raw[0], raw[1:], nil
}