59 Commits
Author SHA1 Message Date
zeekay 7916df8c15 chore: sync working tree
Commits 4 outstanding change(s) that were sitting uncommitted.
No build artifacts and no secrets in the changeset (both checked).
2026-07-26 10:13:52 -07:00
zeekayandHanzo Dev fce2fe8f73 kms: root-level secrets were unfetchable — GET/DELETE now split path optionally
The store keys on (path, name) and accepts an empty path, so List and POST have
always handled secrets at the root of an org. GET and DELETE split `{rest...}` on
the last "/" and returned 400 "path and name required" when there wasn't one —
so every root-level secret was permanently unreachable while List cheerfully
advertised it. Against the same deployed v1.12.9:

  GET /v1/kms/orgs/bootnode/secrets?path=&env=prod
    -> 200 {"names":["DATABASE_URL","API_KEY","APP_SECRET", ...]}
  GET /v1/kms/orgs/bootnode/secrets/DATABASE_URL?env=prod
    -> 400 {"message":"path and name required"}

Two routes over one store disagreeing about what is addressable.

This is why KMS→k8s secret sync has been 100% dead since 2026-03-17 (54/54
KMSSecret CRs failing on lux-k8s). The bootnode org stores its secrets flat, so
the kms-operator could enumerate them and never fetch one. Auth, org scoping and
the operator's native /v1/kms client were all fine — the fetch could not
express "a secret at the root".

Split on the last "/" when there is one; otherwise the whole rest is the name.
An empty name still 400s (now "name required", which is what it means). DELETE
carried the identical split so it gets the identical guarantee.

TestSecretRoutes_RootLevelSecretIsAddressable pins the round trip: list sees it,
get fetches it, delete removes it, 404 after. Verified red/green — without the
handler change it fails with the exact production error.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-26 08:39:09 -07:00
zeekayandHanzo Dev 525646f150 ci: restore the kms build workflow
299b230 deleted this in favour of platform.hanzo.ai native builds (hanzo.yml).
The native plane does not serve yet — the platform Deployment is scaled to 0/0
and PLATFORM_BUILD_CALLBACK_TOKEN, the bearer POST /v1/runner requires, is in
neither cluster — so the cutover left kms the one repo in the fleet that could
not build at all, while every sibling kept building on Actions (luxfi/node has
19 active workflows, hanzoai/gateway 3, hanzoai/commerce 1).

The 422 that motivated the cutover, "Actions has been disabled for this user",
is account-scoped rather than repo-scoped: hanzo-dev gets it, zeekay does not.
Push-triggered runs separately stopped firing around 2026-07-24, so builds want
an explicit dispatch regardless.

hanzo.yml stays and now says plainly that it is intent, not the live path. Cut
over when the native plane serves, and fleet-wide rather than one repo at a time.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-26 01:14:27 -07:00
zeekay cea13fa073 docs: record the v1.12.9 HTTP list route
CLAUDE.md is a symlink to LLM.md, so the edit goes to the real file.
2026-07-26 01:04:49 -07:00
zeekayandHanzo Dev c199becfe4 kms: HTTP secret list — surface parity with the ZAP wire
The ZAP wire has carried OpSecretList (0x0042, {path, env} -> {names}) since it
was written. HTTP had get/put/delete and no list, so the two framings of the
same store disagreed about what you could ask it: a client that wanted to
enumerate had to open a ZAP connection or give up. The hanzo browser extension
gave up and pinned itself to Infisical /api/v3 paths instead.

  GET /v1/kms/orgs/{org}/secrets?path=&env=  ->  {"names": [...]}

Same handler shape and same envelope the ZAP op returns — one store, two
framings, now the same question set.

The pattern has no trailing segment, so ServeMux routes the bare collection here
and anything below it to the existing {rest...} handler. That split is the whole
risk in this change: invert it and list swallows every get, or get 400s on the
bare collection for want of a slash to split. TestSecretRoutes_ListAndGetDoNotShadow
asserts both directions, plus that an empty result is [] and never null.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-26 01:04:30 -07:00
zeekayandHanzo Dev baecefad0d converge: delete the legacy luxfi/kms-operator, and the Infisical NOTICE
Two homes existed for one job — reconciling KMSSecret CRs into k8s Secrets:

  cmd/kms-operator (252 lines) -> ghcr.io/luxfi/kms-operator
  hanzoai/kms-operator (own repo) -> ghcr.io/hanzoai/kms-operator

hanzoai/kms-operator is the one that actually runs, on BOTH clusters
(lux-k8s kms-operator-system/kms-operator-controller-manager and hanzo-k8s
kms-operator-canonical). This repo's own docs already say so:
internal/documents/architecture.md calls hanzoai/kms-operator canonical and the
luxfi name "older"; index.md flags the remaining luxfi pins as drift. The only
thing still pinning ghcr.io/luxfi/kms-operator is lux-devnet/kms-zap-operator,
scaled 0/0.

So this was the dead duplicate. Delete it rather than keep fixing it — I had
just repaired its vendor drift in v1.12.8, which fixed a build for a component
that should not exist. Deleting beats fixing a second way to do one thing.

Also drop NOTICE. It carried the MIT attribution for Infisical-derived code —
required while that code was present, and it was: the vendored Infisical
console. a6a933d removed it. Nothing derived remains (0 TS/JS files, 0
Infisical references across all 60 Go files), so the attribution no longer
applies. The empty frontend/ directories go too.

The KMS service itself is untouched: cmd/kms builds and pkg/keys + cmd/kms
tests pass.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-25 17:51:02 -07:00
zeekayandHanzo Dev 299b2307bf build via platform.hanzo.ai, not GitHub Actions
GitHub answers a workflow dispatch on this repo with 422 "Actions has been
disabled for this user", so pushes to main produced no build at all — the last
run is still the commit before the console removal, and the deployed image
drifted behind main without anything reporting a failure.

hanzo.yml hands the build to platform.hanzo.ai, which schedules it onto the
self-hosted arcd pools over the native fabric and pushes to GHCR. Same path
luxfi/node uses. .github/workflows/build-lux-kms.yml is retired rather than
left as a second, broken way to produce the same image.

No deploy block: the kms Deployment is kustomize-managed with no operator CR,
so there is nothing for a Service-CR rollout to target.

VERSION 1.12.6 -> 1.12.8. The file had fallen behind the deployed tag v1.12.7,
so a tag build would have gone backwards.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-25 17:45:07 -07:00
zeekayandHanzo Dev e5efff89ca ci: drop build-server-self.yml — it builds a deleted Dockerfile
a6a933d ("kms is the Go service on /v1/kms — delete the Infisical console")
removed Dockerfile.server and Dockerfile.web but left .github/workflows/
untouched. build-server-self.yml still passes `file: Dockerfile.server`, so
every run since fails immediately:

  ERROR: failed to solve: failed to read dockerfile:
         open Dockerfile.server: no such file or directory

Delete the workflow rather than repoint it. It existed to build a second,
"self-contained" variant of the same server into ghcr.io/luxfi/kms:server —
a duplicate path for one job. The main Dockerfile (Build KMS) already builds
the server, and the :server image is consumed by nothing: zero pods reference
it on either lux-k8s or hanzo-k8s, and the only remaining mentions are this
workflow and a historical note in hanzo/kms/DEPRECATED.md.

Leaves one Dockerfile and one server build.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-25 17:44:14 -07:00
zeekayandHanzo Dev 1c8b627a25 build: resync vendor/ so the kms-operator image can build again
ghcr.io/luxfi/kms-operator has had no new image since v1.12.3. The Build KMS
Operator workflow failed on every tag since (v1.12.4 … v1.12.7) while its
sibling Build KMS succeeded on the same tag, so the tags looked published.

Dockerfile.operator:27 builds with -mod=vendor, and vendor/ had drifted from
go.mod in both directions:
  - go.opentelemetry.io/otel/{metric,trace}: in vendor/modules.txt, not in go.mod
  - github.com/luxfi/{crypto,geth,ids,keys,zap,address,age,cache,constants,
    container,formatting,math,go-bip32,go-bip39}: in go.mod, not marked explicit

`go mod vendor` alone could not fix it — it aborted first on go.sum entries
missing for the k8s packages cmd/kms-operator imports (k8s.io/api/core/v1,
k8s.io/apimachinery/..., k8s.io/client-go/{dynamic,kubernetes,rest}). So:
go mod tidy (adds them, drops now-unused indirects) then go mod vendor.

Verified by stash control:
  operator, -mod=vendor:  rc=1 -> rc=0   ("inconsistent vendoring" 1 -> 0)
  server,   -mod=vendor:  rc=1 -> rc=1   (unchanged)

The server's vendor build was already broken and stays broken, for an unrelated
reason: blst.h has never been vendored (confirmed absent from HEAD's tree, so it
could not have worked before either) — go mod vendor does not copy cgo headers
that live outside the package dir. It is not a regression and does not affect
CI: the server's Dockerfile builds with GOFLAGS=-mod=mod after `rm -f go.sum &&
go mod download`, i.e. from the module cache, never from vendor/. The operator
is the only -mod=vendor consumer, which is exactly why it was the only failure.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-25 17:39:26 -07:00
zeekayandHanzo Dev d333c66bae kms: JSON only — HTML is not reachable on this listener
Removing the embedded console stopped the SPA answering every unmatched path,
but the mux could still emit HTML on its own: http.ServeMux writes an HTML body
for the implicit trailing-slash redirect, http.Error writes text/plain, and the
default unmatched-route handler writes "404 page not found".

jsonOnly wraps the mux so every response carries application/json and a JSON
body regardless of what the handler underneath wrote. Status and Location are
untouched, so redirects still redirect — they just stop explaining themselves in
HTML. "/" is registered as a JSON 404 carrying the requested path, so a wrong
URL says so in a form a caller can act on.

This is the failure that let genesis call /api/v3/secrets/raw against this host
and read the console page back instead of an error. Four tests pin it: unmatched
paths are JSON 404s, redirect bodies contain no HTML but keep Location, plain
text errors become JSON with the message preserved, and JSON handlers pass
through byte for byte.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-25 17:30:37 -07:00
zeekayandHanzo Dev a6a933dd92 kms is the Go service on /v1/kms — delete the Infisical console
This repo carried two KMS implementations. cmd/kms is a Go service serving
/v1/kms/orgs/{org}/secrets/{path}/{name} out of ZapDB behind IAM-signed
bearers, and its own source calls that "the ONLY HTTP way to read or write
secrets". Alongside it sat frontend/ — 3,083 files of the Infisical console,
calling /api/v1/{dynamic-secrets,secret-scanning,projects}. Two systems, one
name. LLM.md already listed frontend/ as "Legacy — Old React dashboard" and
claimed "NO Node, NO Infisical"; the tree said otherwise.

Deleted frontend/, the go:embed of its build output, registerWebUI, and the
/v1/admin/config endpoint that existed only to bootstrap that SPA. The three
Dockerfiles collapse to one: the server-only build, which the frontend-building
variants existed to work around. Makefile loses build-ui/copy-ui. CI already
pointed at `Dockerfile` and so needs no change.

This also removes a trap. The embedded SPA answered every unmatched path with
HTML, so a wrong URL looked like a 200 instead of a 404 — which is exactly how
genesis called Infisical /api/v3/secrets/raw against this host for however long
and got the console page back instead of an error. The API now 404s honestly.

8,407 tracked files -> 5,320. Every route is /v1/kms/* plus /health{,z}.
Build clean, cmd/kms and root package tests pass.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-25 17:21:38 -07:00
zeekayandHanzo Dev 56debeba46 keys: verify the threshold a generated key actually has, and fail closed
GenerateValidatorKeys validated req.Threshold (>=2, parties >= threshold) and
then never sent it — mpc.KeygenRequest has no threshold field, because the MPC
ring's own --threshold decides the CGGMP21 polynomial degree. The request was
advisory; the result was trusted blind.

That was not a theoretical gap. mpcd's KeygenResult carried neither threshold
nor participants, so `Threshold: blsResult.Threshold` and
`Parties: len(blsResult.Participants)` both evaluated to zero. Every validator
key set on record reads 0-of-0 while its caller requested 3-of-5 and passed
validation. The unit tests missed it because the mock server returned
threshold:3 and five participants — it told the truth the real ring never did.

Add verifyThreshold: one function, called for both the BLS and Corona keys.
It refuses a mismatch in either direction, and refuses just as firmly when the
ring declines to state what it did — silently trusting an unstated threshold is
the exact failure being guarded. Nothing is written to the store on refusal, so
a rejected key leaves behind no record asserting a property never established.

DEPLOY ORDER: requires mpcd >= v1.17.15 (luxfi/mpc 81be6b9), which is what
began reporting these fields. Against an older ring every keygen now fails
closed with an explicit "upgrade the MPC ring" error instead of silently
recording 0-of-0. Roll the MPC quorums first.

Tests: 5 refusal cases (unreported, weaker, degree-0, fewer parties, stronger)
each asserting nothing is stored, plus a positive control proving a matching
3-of-5 is accepted and recorded as 3-of-5.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-25 16:42:10 -07:00
zeekayandHanzo Dev 0c3eca807e fix(kms): self-heal go.sum re-tag in Dockerfile.server (match main Dockerfile)
Dockerfile.server used plain 'go build' (vendor mode) + a single pre-COPY
'go mod download'. Two breakages: (1) the committed vendor/ lags go.mod
(luxfi/vm v1.2.3 in modules.txt vs v1.3.1 in go.mod) -> 'inconsistent
vendoring'; (2) COPY . . re-clobbers any regenerated go.sum with the stale
committed one, so a force-republished luxfi tag aborts the build on a
checksum mismatch.

Mirror the proven-green main Dockerfile: GOSUMDB=off + rm -f go.sum &&
go mod download before AND after COPY . ., and GOFLAGS=-mod=mod on build
so it resolves from the module cache instead of the inconsistent vendor
tree. Regenerate-not-bypass: integrity still enforced against go.sum.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-21 15:38:04 -07:00
zeekayandHanzo Dev 6609a93178 fix(docker): regenerate go.sum after COPY so force-moved luxfi tags don't break the build
The builder already regenerated go.sum under GOSUMDB=off before copying source
(to survive luxfi's periodic tag re-publishes), but `COPY . .` re-introduced the
committed go.sum and silently reverted that regeneration. When luxfi/vm@v1.3.1 was
force-moved, the committed go.sum (h1:rLCby...) stopped matching the bits the CI
runner fetches (h1:U2Bv7...), so `go build -mod=mod` aborted with a checksum
mismatch — the 2026-07 "Build KMS" red that blocks shipping the v1.12.5 env-leak
fix. Regenerate go.sum from the primed cache after COPY so the build verifies
against what it actually fetched. Integrity stays enforced against the regenerated
go.sum (regenerate-not-bypass).

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-21 10:24:26 -07:00
zeekayandHanzo Dev 7116f60fc6 fix(kms): delete unwrapped /v1/kms/secrets/{name} env-leak route (HIGH/CRITICAL)
The route was registered on the bare mux with NO auth middleware and did
os.Getenv(name), returning the raw value. A caller reaching :8080 past the
network boundary (NetworkPolicy gap, port-forward, pod compromise, SSRF) could
`curl .../v1/kms/secrets/KMS_MASTER_KEY_B64` with no Authorization header and
exfiltrate the root REK protecting every per-secret DEK — plus MPC_TOKEN,
KMS_ENCRYPTION_KEY_B64, and (k8s/ variant) the S3 backup keys. On the live
lux-kms-go/kms statefulset KMS_MASTER_KEY_B64 is populated, so this was live
master-key exposure gated only by the network → CRITICAL.

Deleted, not gated: zero callers (kms-operator + kms client read via the
org-scoped path/ZAP; process env is never a secret source). The three
org-scoped secret routes are extracted into registerSecretRoutes so the exact
exposed route set is testable in isolation. Secrets now flow ONLY through
/v1/kms/orgs/{org}/secrets/* (requireOrgJWT) and the ZAP wire.

Regression TestSecretRoutes_NoEnvVarLeak: unauth -> 404, admin -> 404, the
process-env canary is never in the body. Corrected the stale kms.go doc
comment that referenced the route. Full cmd/kms suite green (CGO_ENABLED=0).

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-21 09:30:20 -07:00
zeekayandHanzo Dev 41d2b14198 fix(kms): propagate recovery id v and canonical signature on /sign
The /sign response was {signature, r, s} — no v, S not low-S normalized, and
signature sometimes empty. A high-S signature is rejected by luxfi/evm (EIP-2)
and a missing v forces a recovery-id guess (wrong guess => wrong address).

mpcd now emits EIP-2 low-S R/S, the recovery id V, and the canonical 65-byte
r‖s‖v (via the wired SigEthereum helper). Carry these through:
- pkg/mpc SignResult: parse V from the wire.
- pkg/keys SignResponse + SignWithBLS/SignWithCorona: propagate V. For
  secp256k1 Signature is now the ecrecover-ready r‖s‖v; for ed25519 it stays the
  64-byte blob with empty V. Callers build EVM tx v as 27+V (legacy) or
  chainID*2+35+V (EIP-155). No empty signature field; r/s/v documented.

Tests: TestSignWithBLS asserts v propagates. End-to-end EVM validity proven in
threshold (SigEthereum: 256 random + high-S recover to signer; live devnet
chainID 31337 tx mined status=success).

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-21 08:32:55 -07:00
zeekayandHanzo Dev 3a37dde138 fix(kms): fail-closed app-layer auth on validator-key routes (sign/keygen/rotate)
The /v1/kms/keys/* routes (keygen, sign, rotate, metadata reads) were gated
ONLY by requireMPC — no caller-identity check. The routes were declared
"unprotected at the HTTP layer — auth enforced by Gateway/NetworkPolicy". Any
caller reaching :8080 directly (NetworkPolicy misconfig, pod compromise,
port-forward, SSRF) could sign arbitrary digests with any custodied key, or
keygen/rotate at will. Trusting a Gateway-injected role header is not enough:
a direct caller forges the header; only an IAM signature cannot be forged.

Add requireKeyAuth (orgJWTAuth): verifies the IAM-signed JWT HERE against
JWKS (sig + issuer + expiry) and requires the kms-admin role (superadmin is
break-glass) — the same write authority the zapserver /v1/sdk sign op and the
mpc KMS handlers already enforce. Fail closed at every step: nil auth => 503,
missing/malformed/bad token => 401, authenticated-but-no-role => 403. This is
defense in depth BEHIND the NetworkPolicy, never a replacement for it. The
/v1/kms/status health probe stays open (no key material).

Tests (SDKROOT="$(xcrun --show-sdk-path)" go test ./cmd/kms):
  requireKeyAuth: missing bearer 401, malformed 401, nil-auth 503 (fail
  closed), valid-token-without-role 403, kms-admin 200, superadmin 200.
  failopen suite updated to authenticate (kms-admin) so it still pins the
  requireMPC 503/recovery path behind the new gate. Full cmd/kms: ok.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-21 08:19:17 -07:00
zeekay ec832e1465 chore(deps): bump geth v1.20.1 + luxfi deps — stack unification 2026-07-21 08:19:17 -07:00
Hanzo Dev 947419bf28 chore: complete go.sum module hashes
Add the missing h1: zip checksums for already-required modules so the
module graph is fully pinned. go mod verify passes.
2026-07-19 19:51:55 -07:00
zeekayandHanzo Dev 051a078025 docs(LLM): KMS<->MPC wire fix (v1.12.3) proven + authorizer-coupling deploy caveat
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-14 18:04:59 -07:00
zeekayandHanzo Dev 7a677ae571 fix(docker): GOSUMDB=off + in-build go.sum regen for re-published luxfi modules
The ecosystem re-tag re-published luxfi/keys@v1.1.0 and luxfi/pq@v1.0.3
under fixed versions (proxy now serves bits that the immutable
sum.golang.org rejects), and wiped geth v1.16.x. The builder's plain
'go mod download' against the committed go.sum + default sumdb therefore
failed ('checksum mismatch'). Adopt luxfi/mpc's proven builder pattern:
GOSUMDB=off + rm+regenerate go.sum from the current proxy/authed-git
source (integrity still enforced against go.sum). Commit a go.sum carrying
the current proxy hashes for local dev. Pairs with the go.mod geth
v1.17.12 bump.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-14 16:37:34 -07:00
zeekayandHanzo Dev e361c1258d fix(deps): post-re-tag coherent luxfi set (geth v1.17.12), re-vendor
luxfi/geth wiped its v1.16.x line (v1.16.98 gone) in the ecosystem
re-tag, breaking 'go mod download' at Dockerfile:27. Move to the coherent
set crypto v1.19.27 -> geth v1.17.12 (both proxy-resolvable), regenerate
go.sum against the public proxy (matches the Dockerfile's proxy fetch),
and re-vendor so the in-tree vendor/ stays consistent. Pairs with the
already-landed KMS<->MPC ZAP wire fix (7377ad6) so a tagged release both
builds and signs against mpcd.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-14 16:26:04 -07:00
zeekayandHanzo Dev 7377ad63d2 fix(mpc): realign KMS↔MPC ZAP signing wire to mpcd contract; kill false-green
The KMS ZapClient and the mpcd KMS-facing ZAP server (luxfi/mpc
pkg/api/zap_kms_server.go) had drifted out of lockstep despite the
"MUST stay in lockstep" comment — each was only unit-tested against its
own stub, never against the other's real bytes. Three drifts:

1. Sign request: KMS sent {key_type, wallet_id, message}; mpcd requires
   {vault_id, wallet_id, payload} and hard-fails "vault_id and wallet_id
   required". SignRequest now carries VaultID (Manager supplies it from
   MPC_VAULT_ID) and Payload (json "payload").
2. Keygen response: mpcd returns snake_case (wallet_id/ecdsa_pub_key/
   evm_address); KMS KeygenResult was camelCase → decoded to an empty
   struct. Tags realigned to snake_case (the ZAP path is the only live
   decode; the REST Client is unused).
3. False-green: ZapClient.call() json-decoded the daemon's {"error":...}
   frame into a zero-value SignResult and returned it with nil error —
   the KMS looked MPC-backed but returned an empty signature. call() now
   surfaces a non-empty top-level "error" as a real error (fail closed).

Adds pkg/mpc/wire_contract_test.go — a cross-repo contract test that
transcribes the mpcd request/response shapes and fails CI if the KMS
side drifts again (the guard that was missing). Existing tests updated
to the corrected snake_case wire.

This is a library fix in luxfi/kms — it fixes the wire for all three
orgs (Lux/Zoo/Hanzo) once. ed25519/Corona keygen over this wire remains
a follow-up (mpcd's KMS-ZAP keygen carries no key_type/protocol).

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-14 14:15:05 -07:00
zeekayandHanzo Dev 415097f535 fix(kms): accept multiple white-label issuers (lux.id + hanzo.id)
KMS_EXPECTED_ISSUER now takes a comma-separated allowlist so one KMS can
serve several white-label brands that share IAM signing keys. The Lux-brand
KMS (kms.lux.network) must accept lux.id tokens — lux.cloud users carry
iss=https://lux.id — while still honoring hanzo.id. JWKS is unchanged
(fetched in-cluster; both brands sign with the same keys), so this is a pure
issuer-string allowlist: parse+dedupe, then validate membership after the
signature/expiry check. Backward compatible: a single-value
KMS_EXPECTED_ISSUER behaves exactly as before.

Deploy: KMS_EXPECTED_ISSUER=https://hanzo.id,https://lux.id on the Lux KMS.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-14 10:15:00 -07:00
hanzo-dev 8d066a99ae ci: route linux/amd64 jobs to lux-build-amd64 ARC scale set 2026-07-10 21:54:20 -07:00
2707692bc8 fix(kms): require explicit env on secret writes (no silent default) (#13)
POST /v1/kms/orgs/{org}/secrets defaulted a missing env to "default",
committing the write to a bucket that project/env/path readers (the
kms-operator, cluster syncs) never resolve. That split landed an IAM
z-password in env=default while prod kept serving the stale value. env is a
first-class component of the storage key (kms/secrets/{path}/{env}/{name})
and cannot be aliased, so a write with no env now fails loud (400). GET
keeps a backward-compatible default (a read can't plant a value another
reader trusts; legacy readers that omit env must keep working).

The POST handler is extracted to a named func (putSecretHandler) so the
contract is unit-testable. Regression tests: write without env -> 400;
write env=prod is stored under prod (sha256 round-trip, values never
printed) and is not visible in env=default.


Co-authored-by: hanzo-dev <dev@hanzo.ai>
2026-07-08 07:21:43 -07:00
b54c590a1e fix(deps): luxfi/keys@v1.1.0 go.sum = current proxy/sumdb hash (re-tag) (#12)
The module was re-tagged; go.sum pinned the stale hash so every CI build
failed at go mod download (checksum mismatch / SECURITY ERROR). Refetched
clean — the new hash is sumdb-verified.

Co-authored-by: hanzo-dev <dev@hanzo.ai>
2026-07-04 11:51:03 -07:00
9b0d48d836 fix(auth): kid-first JWT verification, drop per-attempt debug logging (#11)
validateToken logged one line per JWKS key per request (a leftover DEBUG line
plus a per-failed-key line) — with multi-brand certs that produced a log line
per brand cert per request, ~4.4M lines/12h in production, and brute-forced
every key on every call. Now: verify against the key the token's kid header
names (single signature check in the common case), fall back across the
remaining keys only on rotation skew, and emit no per-attempt logs — the
returned error carries the diagnosis.

Co-authored-by: hanzo-dev <dev@hanzo.ai>
2026-07-04 11:50:16 -07:00
ec13c353fb fix(auth): kid-first JWT verification, drop per-attempt debug logging (#10)
validateToken logged one line per JWKS key per request (a leftover DEBUG line
plus a per-failed-key line) — with multi-brand certs that produced a log line
per brand cert per request, ~4.4M lines/12h in production, and brute-forced
every key on every call. Now: verify against the key the token's kid header
names (single signature check in the common case), fall back across the
remaining keys only on rotation skew, and emit no per-attempt logs — the
returned error carries the diagnosis.

Co-authored-by: hanzo-dev <dev@hanzo.ai>
2026-07-04 11:28:28 -07:00
343872f5da feat: native /v1/sdk enveloped secrets + threshold-sign plane (red-approved) (#9)
* test(zapserver): fix e2e seed/assert mismatch (secret-value vs sk_live_real)

TestConsensusE2E_ValidatorReadsSecret seeded "secret-value" but asserted
"sk_live_real" — the E2E read path was correct, the assertion constant
disagreed with the seed. Align the seed to the asserted live-secret value
so the green test actually guards the consensus read path.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>

* feat(zapserver): expose enveloped secret+sign plane over HTTP /v1/sdk

Decomplect the verify->authorize->dispatch core so the ZAP wire and a new
HTTP /v1/sdk transport share ONE implementation (Server.dispatch). Add:

- POST /v1/sdk/secrets: single RPC endpoint. The op is read from the
  SIGNED env.Op field, never the URL, so no framing can escalate a read
  identity into a write. Maps status byte -> HTTP code; replay masked as
  generic 403 'forbidden' (nonce ledger unprobeable); 4 MiB body cap.
- OpSign (0x0050, write/operator) + OpVerify (0x0051, read/validator):
  deliberate, documented widening of the authorizer. Dispatch to a narrow
  SignBackend that delegates to luxfi/mpc t-of-n; KMS holds no key
  material. nil backend -> clear 'signing not configured'.

18 new httptest end-to-end tests: validator read / operator write splits,
replay/stale/tamper/oversize/malformed/unknown-op rejection, and sign
delegation gated BEFORE the backend (forbidden sign never reaches MPC).
All prior authz tests stay green.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>

* feat(sdksign): keys.Manager -> zapserver.SignBackend adapter

Sign delegates to the MPC t-of-n cluster (KMS holds no full key). Verify
is a local public-key check: ed25519 (corona) via stdlib crypto/ed25519
(real, tested with a live keypair); secp256k1 (bls) returns a precise
ErrVerifyBLSDelegated — a documented capability boundary (secp256k1
verification is owned by the chain/precompile layer), not a fake.

Tests (CGO=0, no MPC daemon needed): sign routes to the correct wallet
per scheme, result propagates; ed25519 valid verifies + tampered
sig/message do NOT; bls verify hits the delegated boundary; unknown
validator errors.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>

* fix(gomod): realign age@v1.5.0 + keys@v1.1.0 go.sum to re-tagged hashes

Both tags were force-moved (vendor/docs sync lineage, per LLM.md); go.sum
held the pre-re-tag zip hashes, so any GOWORK=off / clean-cache build
(CI) failed 'checksum mismatch' before compiling. Updated the two h1
lines to the authoritative direct-fetch hashes; the GOWORK=off build
re-verifies the downloaded bits against these, which is the proof they
are correct. Never bypassed the check.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>

* feat(kms): mount /v1/sdk enveloped secrets+sign surface in kmsd

Construct the zapserver.Server once from the loaded REK and back BOTH
transports with ONE consensus authorizer + ONE nonce ledger: the HTTP
/v1/sdk surface (SDK-facing) and the in-cluster ZAP wire. The authorizer
and ledger are built whenever the REK is present (fail-closed: refuse to
boot without them), independent of ZAP_PORT — so /v1/sdk is available
even when the ZAP wire is disabled. Wire the MPC-backed sign/verify via
sdksign.New(mgr) when a vault is configured; nil otherwise.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>

* docs(llm): document /v1/sdk enveloped secrets+sign surface

Co-authored-by: Hanzo Dev <dev@hanzo.ai>

---------

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-03 10:37:07 -07:00
f9d4832cdf docs(LLM): finished web UI reality + canonical image (#7)
* docs(LLM): state finished web UI reality + canonical image

* feat(oidc): derive KMS SSO client id+secret from brand (zero env, matches IAM)

loadOIDCConfig now DERIVES the confidential SSO client from KMS_IAM_OWNER:
client_id=<owner>-kms, client_secret=HMAC-SHA256("hanzo-kms-oidc/v1",<owner>-kms),
state_secret=HMAC("hanzo-kms-state/v1",<owner>-kms). Same derivation hanzoai/iam
init_apps.go uses, so IAM provisions and KMS consumes the identical secret with
ZERO coordination — no KMS_OIDC_CLIENT_SECRET/KMS_STATE_SECRET env, no stored
secret, no manual copy. Env still overrides each (escape hatch). Only the
non-secret IAM_ENDPOINT remains required. Closes the deterministic KMS-admin
loop: reboot IAM+KMS -> SSO works -> admins mint scoped identities.

Note: full repo build is blocked by the pre-existing luxfi/* go.sum re-publish
mismatch (luxfi/age, luxfi/keys); oidc.go itself is gofmt-clean and the
derivation is parity-verified against the IAM side.

---------

Co-authored-by: hanzo-dev <dev@hanzo.ai>
2026-07-01 10:35:21 -07:00
zeekayandHanzo Dev 3b9334e26f feat(mnemonic): canonical BIP-39 bootstrap-mnemonic loader over native ZAP
One canonical path for every Lux-derived service (luxd, netrunner, cli,
descending L1 bootstrap) to resolve the bootstrap mnemonic: MNEMONIC env
wins for local/CI; KMS over native ZAP is the production fallback. Lives
in luxfi/kms (not luxfi/keys) to keep the keys->kms edge from closing an
import cycle. Real guard test covers env-precedence (no KMS dial when env
set), BIP-39 validation reject, KMS-fake valid/empty paths, and the
SplitSecretPath addressing convention.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-06-30 16:00:59 -07:00
zeekayandHanzo Dev 95cdad9ee5 feat(mnemonic): add pkg/mnemonic.Load — cycle-free KMS bootstrap mnemonic loader
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-06-30 15:56:29 -07:00
z 888d5f5a78 docs(brand): add hero banner 2026-06-28 20:36:07 -07:00
z 91a16411a9 chore(brand): dynamic hero banner 2026-06-28 20:36:06 -07:00
hanzo-dev a20bc707f1 build: drop GOPRIVATE from Dockerfiles — use immutable public proxy+sumdb
The Docker builds set GOPRIVATE=luxfi/*,hanzoai/* which routes module fetches
'direct' (git) and bypasses sum.golang.org. After luxfi/keys@v1.1.0 was
force-republished, the git tag content diverged from the immutable proxy/sumdb
hash — so 'go mod download' fetched bits that no longer matched the (correct,
sumdb-canonical) go.sum, failing every image build with SECURITY ERROR checksum
mismatch. These modules are public; resolving them through the default public
proxy + sumdb yields the canonical immutable hash that matches go.sum and a
re-publish can no longer break. GITHUB_TOKEN insteadOf stays for the direct
fallback.
2026-06-28 14:15:10 -07:00
hanzo-dev 5ac2347a04 fix(deps): sync go.sum to canonical public sumdb hashes (no GOPRIVATE/sum hacks)
The committed go.sum carried stale hashes for re-published luxfi modules,
because the dev go env used GOPROXY=direct + GONOSUMDB/GOPRIVATE for these
PUBLIC repos — bypassing the immutable proxy + checksum db, so a force-moved tag
silently poisoned the cache. luxfi/age, luxfi/keys et al are public; routing
them through the standard public proxy (proxy.golang.org) + sumdb gives
canonical immutable hashes that a re-publish can no longer break. go.sum now
matches sum.golang.org; 'go mod verify' = all modules verified; go.mod
unchanged.
2026-06-28 14:09:17 -07:00
1cffefaa57 build: fix keys@v1.1.0 go.sum to authoritative direct hash (unblocks KMS build) (#8)
luxfi/keys v1.1.0 was re-published; go.sum still pinned the original D3s754…
zip hash → 'go mod download SECURITY ERROR' in CI/clean builds. Updated to the
authoritative direct-from-GitHub hash /3mQfl… (same fix applied to hanzoai/iam
this cycle). age@v1.5.0 already correct. Builds clean (CGO sqlcipher).

Co-authored-by: hanzo-dev <dev@hanzo.ai>
2026-06-28 13:48:55 -07:00
hanzo-dev 00c5b6b6c6 feat(oidc): derive KMS SSO client id+secret from brand (zero env, matches IAM)
loadOIDCConfig now DERIVES the confidential SSO client from KMS_IAM_OWNER:
client_id=<owner>-kms, client_secret=HMAC-SHA256("hanzo-kms-oidc/v1",<owner>-kms),
state_secret=HMAC("hanzo-kms-state/v1",<owner>-kms). Same derivation hanzoai/iam
init_apps.go uses, so IAM provisions and KMS consumes the identical secret with
ZERO coordination — no KMS_OIDC_CLIENT_SECRET/KMS_STATE_SECRET env, no stored
secret, no manual copy. Env still overrides each (escape hatch). Only the
non-secret IAM_ENDPOINT remains required. Closes the deterministic KMS-admin
loop: reboot IAM+KMS -> SSO works -> admins mint scoped identities.

Note: full repo build is blocked by the pre-existing luxfi/* go.sum re-publish
mismatch (luxfi/age, luxfi/keys); oidc.go itself is gofmt-clean and the
derivation is parity-verified against the IAM side.
2026-06-28 13:45:06 -07:00
zeekay 50ad9fbd51 remove Stripe from CSP + de-brand test fixtures (Stripe account disabled)
frontend CSP no longer needs to allow *.stripe.com (Infisical-era billing
UI we don't use). Drop all stripe.com sources + the stripe-only child-src.
Rename the Stripe-flavored example secret in zapserver/envelope tests to a
neutral api-key/secret-value fixture. No Go code paths change.
2026-06-24 12:36:52 -07:00
Hanzo 6dd26decd0 rebrand: neutralize legacy upstream brand across UI, backend, docs; add NOTICE
Replace all inline references to the legacy upstream project with Lux KMS
branding (prose/labels), neutral identifiers (code), and neutral string
literals (frontend-internal cache keys, sentinels, masks), with every
reference renamed consistently so behavior is unchanged. Dead networking
UI (relay/gateway/pam) CLI examples now reference the Lux CLI.

Add a NOTICE file carrying the required upstream MIT attribution and full
permission notice — the only place the upstream name remains, as required
by that license.

No functional changes. Frontend (vite build) and Go backend
(go build -tags sqlite_fts5 sqlcipher) both build green.
2026-06-24 11:19:41 -07:00
zeekay 944e8a3462 docs(kms): record go.sum re-tag fix + keys↔kms phantom-cycle analysis in LLM.md 2026-06-24 10:32:58 -07:00
zeekay 2e949dfbf3 fix(kms): re-tag go.sum for luxfi/age@v1.5.0 (second re-tagged dep)
After the keys@v1.1.0 go.sum fix (1889442), the Build KMS run for
v1.9.14 surfaced a SECOND re-tagged module at 'go mod download':
luxfi/age@v1.5.0 was force-moved the same way (the 'fa86744 vendor:
sync luxfi/keys + luxfi/age docs' lineage), so CI's direct-from-GitHub
fetch computed h1:zC/Fw/… while go.sum pinned the stale
h1:G69HbSV4… → checksum mismatch.

Updated the one age h1 line to the authoritative current hash
(verified from a pristine GOMODCACHE; the /go.mod hash was unchanged).
A full pristine 'go mod download' of the entire module graph (CI
fidelity: isolated cache, GOPROXY=direct, GOWORK=off) is now CLEAN —
no remaining checksum mismatches. age is not compiled into the
cmd/kms server binary (transitive verify-only dep).
2026-06-24 10:31:23 -07:00
zeekay 1889442402 fix(kms): re-tag go.sum for keys@v1.1.0 + org sub-scope auth (lux→lux-infra)
Build (Build KMS / Dockerfile, -mod=mod) failed 17 days at 'go mod
download': luxfi/keys@v1.1.0 was re-tagged (force-moved to a different
commit) after kms's go.sum was written, so CI's direct-from-GitHub
fetch (GOPRIVATE) computed h1:D3s754vT… while go.sum pinned the stale
h1:a4UkVVg6… → 'SECURITY ERROR: checksum mismatch'. Regenerated the one
go.sum line to the authoritative current hash (no security bypass).

The keys↔kms 'cycle' (keys@v1.1.0 → kms@v1.9.12, a tag that does not
exist) does NOT block this build: the package graph is acyclic
(go list -deps ./pkg/zapclient has zero luxfi/keys; pkg/envelope is
interface-decoupled, keys appears only in its _test files), and MVS
resolves the kms self-module from the local tree, never fetching the
phantom v1.9.12. The production server binary (cmd/kms) and operator
(cmd/kms-operator) compile ZERO external luxfi/keys (cmd/kms uses the
in-repo pkg/keys; only the cmd/smoke-replay dev tool pulls external
keys) — so no keys content enters the shipped images.

org sub-scope auth: the lux-operator queries KMS with org=lux-infra
(its Infisical projectSlug) but the lux-kms token's org claim is 'lux'
→ strict equality returned 403. orgAuthorizes() now admits a requested
org that equals the token org OR is a hyphen-delimited sub-scope of it
(lux authorizes lux-infra/lux-mainnet/…; the '-' boundary keeps lux
from leaking into luxx-infra). One function, one call site.

Tests: cmd/kms ok (14 TestRequireOrgJWT incl. 2 new sub-scope cases),
pkg/{envelope,zapclient,zapserver} ok.
2026-06-24 10:14:53 -07:00
z 7b37424326 ci(kms): build-lux-kms runs-on lux-build (ARC scale-set label)
runs-on [self-hosted,linux,amd64] never matches the lux-build ARC
AutoscalingRunnerSet (its runner label is the scale-set name 'lux-build'),
so Build KMS jobs queued forever with 'assigned job=0'. Same fix already
applied to build-operator.yml.
2026-06-24 09:40:47 -07:00
zandzeekay 61770f655f fix(kms): post client_credentials/auth_code to /v1/iam/oauth/token
The hanzoai/iam OAuth API moved under the /v1/iam prefix. The legacy
root /login/oauth/access_token is now a frontend redirect, not the
token API, so the KMS auth-login proxy and OIDC SSO code-exchange both
got HTML back -> empty access_token -> 401 'invalid credentials' to the
kms-operator (breaking every KMSSecret sync, incl. luxd-staking on
devnet/testnet/mainnet).

Route both call sites to the canonical token endpoint via a single
env-overridable knob IAM_TOKEN_PATH (default /v1/iam/oauth/token), so a
future path move needs no rebuild. cmd/kms tests updated + green.
2026-06-24 09:35:41 -07:00
Hanzo AI fa86744f90 vendor: sync luxfi/keys + luxfi/age docs with upstream wording 2026-06-24 08:21:46 -07:00
zeekay d912da333c fix(operator): COPY in-module deps (pkg/envelope, pkg/zap) into build context
zapclient imports github.com/luxfi/kms/pkg/{envelope,zap}, which are the
module's own packages. -mod=vendor only vendors external deps, so these
must be present in the Docker build context as source. The selective COPY
missed them, so the build failed:

  pkg/zapclient/client.go:34:2: cannot find module providing package
    github.com/luxfi/kms/pkg/envelope: import lookup disabled by -mod=vendor

COPY both packages; also add them to the workflow paths filter so the
operator image rebuilds when they change.
2026-06-22 08:13:46 -07:00
zeekay bf7f5f97f6 ci(kms-operator): runs-on lux-build (ARC) — classic self-hosted amd64 label is offline for luxfi/kms 2026-06-22 05:12:03 -07:00
zeekay bd804cb17c ci(kms-operator): build + push the ZAP-transport KMSSecret reconciler
The deployed kms-operator (ghcr.io/luxfi/kms-operator:0d446b0) is the legacy
Infisical-flavor HTTP operator: it POSTs /api/v1/auth/universal-auth/login,
which the Go KMS server does NOT expose, so every KMSSecret sync fails with
CallUniversalAuthLogin 404 (e.g. lux-devnet/luxd-staking-kms-sync). The fix —
cmd/kms-operator, which reconciles over the native luxfi/zap binary transport
on port 9999 (no HTTP auth round-trip) — already exists in source but had no
CI to ship it. Add a self-contained build for Dockerfile.operator (same
pattern as build-lux-kms.yml), publishing ghcr.io/luxfi/kms-operator.
2026-06-22 03:43:31 -07:00