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>
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>
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>
* 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>
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>
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.
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.
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.