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>
* 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>
* 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>
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>
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.
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.
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>
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.
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.
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.
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).
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.
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.
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.
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.
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.
cloudbuild.kms.yaml built and pushed lux/kms to 's GAR
(us-docker.pkg.dev/-devnet) — a downstream deployment artifact
that doesn't belong in lux OSS (lux builds to ghcr.io/luxfi via arcd).
Remove it; genericize the / strings in zapclient +
secrets doc comments.
Replaces non-canonical scale-set / org-prefixed labels with the
existing labels every arcd host registers with. Matches evo for
amd64 and spark for arm64. No new labels added.
Squashes all pre-rewrite history into a single root commit. Previous tree:
- 14506 commits including upstream Infisical fork history
- Multiple merge chains carrying pre-purge content
Force-rewrite per disk-space reclamation discipline. GHCR images
(1.9.9, 1.9.11) remain immutable; their git lineage is no longer
materialized.