10 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 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 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 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
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
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
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
zeekay b0a12cf63f kms: drop deployment leak + genericize secret-path examples
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.
2026-06-10 21:47:06 -07:00
Hanzo AI da574aa1db lux/kms: orphan history at f2e793bf8 (was: kms: wire luxfi/mpc as REK backend)
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.
2026-06-07 14:28:09 -07:00