mirror of
https://github.com/hanzoai/kms.git
synced 2026-10-17 11:18:16 +00:00
* docs(llm): explicit Upstream attribution line
LICENSE-attested or repo-attested upstream is now called out at the
top of LLM.md alongside the project description, so downstream
audits (universe/docs/PRODUCTS.md) can rely on a single canonical
location for the OSS lineage statement.
* feat: cloud Mount() per HIP-0106
Adds pkg/kms.Mount(*zip.App, cloud.Deps) error so the unified Hanzo
Cloud binary can blank-import and register the in-process KMS
server alongside every other Hanzo subsystem.
- pkg/kms/mount.go: native /v1/kms/health + /v1/kms/readyz zip
handlers and zip.AdaptNetHTTP wrap of the existing Embed mux at
/v1/kms/* + /healthz. init() registers with cloud.Register("kms",
10, …) so blank import wires the subsystem in.
- cmd/kmsd: rewritten as a HIP-0106 thin shim — cloud.LoadConfig +
cloud.BuildDeps + zip.New + kms.Mount + app.Listen. The Embed
surface is unchanged; only assembly moves through zip.
- schema/kms.zap: minimal Health ZAP interface; full secret/key
surfaces land in follow-up PRs.
Standalone kmsd binary still ships via the existing Dockerfile and
CI workflow.
* refactor: collapse pkg/kms/ → root for clean import path
Subsystem code lives in package kms at repo root. Importers now use
`github.com/hanzoai/kms` (no /pkg/kms/ nesting). Standalone shim moved
to cmd/kms/ where applicable.
Per Hanzo Go-stdlib pattern: repo IS the package.
* refactor: flatten import path — github.com/hanzoai/cloud (drop /pkg/cloud)
110 lines
3.8 KiB
Go
110 lines
3.8 KiB
Go
// Package main — secret version tracking for replay protection (R-3).
|
|
//
|
|
// The upstream luxfi/kms store.Secret has no version field. We store a
|
|
// monotonic int64 version alongside each secret in ZapDB under a sibling
|
|
// key prefix `kms/versions/{path}/{env}/{name}` so upstream schema stays
|
|
// untouched — no fork, no PR, no schema migration.
|
|
//
|
|
// Threat model (R-3): a captured Update envelope replayed after a
|
|
// subsequent rotation would overwrite a live secret with a stale value
|
|
// (revert-on-replay). Create is safe to replay because POST is an upsert
|
|
// of the new value; an adversary cannot use it to revert.
|
|
//
|
|
// Fix: Update (PATCH) requires the caller to supply the secret's current
|
|
// version (If-Match header or JSON body `version`). Mismatch → 409. Each
|
|
// successful write increments the stored version. Create (POST) seeds
|
|
// version = 1 on insert and bumps version on upsert — idempotent semantics
|
|
// preserved (no information leak; adversary cannot cause revert because
|
|
// PATCH is the only mutate-with-prior path and it requires version).
|
|
package kms
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
|
|
badger "github.com/luxfi/zapdb"
|
|
)
|
|
|
|
// ErrVersionMismatch is returned from storePutWithVersion when the caller
|
|
// supplies a version that does not match the current on-disk version.
|
|
var ErrVersionMismatch = errors.New("kms: version mismatch")
|
|
|
|
// versionKey returns the ZapDB key used to store the monotonic version
|
|
// counter for a (path, name, env) secret. Kept parallel to the store's
|
|
// own layout so a list/scan by prefix still works.
|
|
func versionKey(path, name, env string) []byte {
|
|
return []byte(fmt.Sprintf("kms/versions/%s/%s/%s", path, env, name))
|
|
}
|
|
|
|
// readVersion returns the current version of a secret, or 0 if no version
|
|
// has been recorded (equivalent to "secret does not exist yet").
|
|
func readVersion(db *badger.DB, path, name, env string) (int64, error) {
|
|
var v int64
|
|
err := db.View(func(txn *badger.Txn) error {
|
|
item, err := txn.Get(versionKey(path, name, env))
|
|
if errors.Is(err, badger.ErrKeyNotFound) {
|
|
return nil // v stays 0
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return item.Value(func(buf []byte) error {
|
|
if len(buf) != 8 {
|
|
return fmt.Errorf("kms: malformed version record (len=%d)", len(buf))
|
|
}
|
|
v = int64(binary.BigEndian.Uint64(buf))
|
|
return nil
|
|
})
|
|
})
|
|
return v, err
|
|
}
|
|
|
|
// bumpVersion atomically reads-then-writes the version counter, returning
|
|
// the NEW version written. Requires the caller-supplied expected version
|
|
// to match the current version. If expected is -1, skip the CAS check
|
|
// (used by POST/upsert; POST does not claim to know the prior version).
|
|
func bumpVersion(db *badger.DB, path, name, env string, expected int64) (int64, error) {
|
|
var newVer int64
|
|
err := db.Update(func(txn *badger.Txn) error {
|
|
var cur int64
|
|
item, err := txn.Get(versionKey(path, name, env))
|
|
switch {
|
|
case errors.Is(err, badger.ErrKeyNotFound):
|
|
cur = 0
|
|
case err != nil:
|
|
return err
|
|
default:
|
|
if valErr := item.Value(func(buf []byte) error {
|
|
if len(buf) != 8 {
|
|
return fmt.Errorf("kms: malformed version record (len=%d)", len(buf))
|
|
}
|
|
cur = int64(binary.BigEndian.Uint64(buf))
|
|
return nil
|
|
}); valErr != nil {
|
|
return valErr
|
|
}
|
|
}
|
|
if expected >= 0 && expected != cur {
|
|
return ErrVersionMismatch
|
|
}
|
|
newVer = cur + 1
|
|
buf := make([]byte, 8)
|
|
binary.BigEndian.PutUint64(buf, uint64(newVer))
|
|
return txn.Set(versionKey(path, name, env), buf)
|
|
})
|
|
return newVer, err
|
|
}
|
|
|
|
// deleteVersion removes the version record when a secret is deleted, so
|
|
// that a later re-create starts from version 1 again (not version N+1).
|
|
func deleteVersion(db *badger.DB, path, name, env string) error {
|
|
return db.Update(func(txn *badger.Txn) error {
|
|
err := txn.Delete(versionKey(path, name, env))
|
|
if errors.Is(err, badger.ErrKeyNotFound) {
|
|
return nil
|
|
}
|
|
return err
|
|
})
|
|
}
|