Commit Graph
742 Commits
Author SHA1 Message Date
zeekayandGitHub c2ef7641fa Merge pull request #6 from luxfi/fix/gosum-edwards25519
go.sum: refresh filippo.io/edwards25519 checksum
2026-05-15 15:14:24 -07:00
zeekay 6c3a992835 go.sum: refresh filippo.io/edwards25519 checksum 2026-05-15 15:13:53 -07:00
zeekay b5e4df1e7e go.sum: fix luxfi/[email protected] hashes (match sum.golang.org) 2026-05-15 14:59:36 -07:00
zeekay 5d9ae0169e mpc: drop Ringtail identifiers — wire API renamed to Corona
The two MPC files that still carried Ringtail names:
  cmd/mpcd/main.go        — comment ref to luxfi/ringtail
  pkg/api/handlers_kms.go — ValidatorKeySet wire schema

handlers_kms.go renames:
  ValidatorKeySet.RingtailWalletID   → CoronaWalletID
  ValidatorKeySet.RingtailPublicKey  → CoronaPublicKey
  JSON tags ringtailWalletId/PubKey  → coronaWalletId/PubKey
  local `ringtailWalletID` and the per-validator suffix
  "validator-<id>-ringtail"          → "validator-<id>-corona"
  router switch `case "ringtail":`   → `case "corona":`

cmd/mpcd/main.go: comment "luxfi/corona and luxfi/ringtail" was
collapsed to just "luxfi/corona" — the dispatcher's not-yet-impl
stub now references only the live R-LWE library.

Wire break, no compat layer (forwards-perfection policy):
KMS callers must re-issue requests with the new field names.

Build: go build ./... PASS.
Tests: pkg/api PASS 1.374s.
2026-05-14 09:34:28 -07:00
zeekay b3b083c6d5 mpcd: gate embedded threshold dispatcher with bearer-token + loopback bind
Red round 5 finding B1. The embedded dispatcher on --threshold-listen
was an anonymous local signing oracle:

  1. Any process on the host could POST `{"method":"frost.sign",...}`
     and obtain signatures with no auth gate (only the API on :9800
     enforced MPC_INTERNAL_API_KEY).
  2. An operator typo `MPC_THRESHOLD_LISTEN=0.0.0.0:7300` exposed it
     cluster-wide.
  3. Any SSRF inside mpcd pivoted into the dispatcher.

Fix mirrors the existing internal-API auth pattern (cmd/mpcd/main.go
line ~456):

  - Auth token: read MPC_THRESHOLD_AUTH_TOKEN; if unset, derive a
    deterministic per-cluster token from the node Ed25519 seed
    (SHA-256(seed || "mpc-threshold-rpc") hex). All nodes share the
    same key without extra config; log warns when derivation is used.
    Server.SetAuthToken installs the gate.
  - Loopback bind: refuse non-loopback addr unless
    MPC_THRESHOLD_ALLOW_REMOTE=1 — operators must explicitly
    acknowledge the exposure. Bare `:port` (= 0.0.0.0) is NOT loopback.

Also picks up luxfi/threshold v1.6.10 which removes the pulsar/corona
in-memory-token surface (Red HIGH B2) — pulsar.*/corona.* now return a
typed "not yet implemented" error.

Tests:
  - TestIsThresholdLoopback locks the loopback predicate (8 cases:
    127.0.0.1, [::1], localhost OK; 0.0.0.0, :port, RFC1918 refused).
  - TestEmbeddedDispatcher_AuthGate proves 401 on missing/wrong token,
    200 on valid token through the dispatcher mpcd installs.

4 tests pass in cmd/mpcd (was 2, +2).
2026-05-13 16:46:22 -07:00
zeekay 0ea64a63e7 mpcd: embed luxfi/threshold/pkg/thresholdd dispatcher (six schemes)
mpcd is the production threshold daemon. With this commit it embeds
the JSON-RPC dispatcher from luxfi/threshold/pkg/thresholdd on a new
--threshold-listen flag (default 127.0.0.1:7300, MPC_THRESHOLD_LISTEN
env var). The dispatcher exposes every supported scheme:

  cggmp21 (secp256k1 ECDSA, CGGMP21 fork)
  frost   (secp256k1 Schnorr, RFC 9591)
  bls     (BLS12-381 Shamir/Lagrange)
  sr25519 (Ristretto255, via mpc node's own session — separate path)
  pulsar  (M-LWE / FIPS 204 ML-DSA-65 threshold, via luxfi/corona/threshold)
  corona  (R-LWE / Ringtail, via luxfi/ringtail/threshold)
  doerner (reserved; upstream protocol non-functional, returns explicit error)

Wire format matches teleport/mpc/src/signers/rpc.ts byte-for-byte:

  POST /  {"jsonrpc":"2.0","id":N,"method":"<scheme>.<op>","params":{...}}

Methods per scheme: keygen / sign / verify. Public-key handles are
returned by keygen and bound on sign/verify.

Decomplecting note: the dispatcher carries zero policy. Profile
gating, auth, audit, and rate-limiting live on mpcd's existing API
surface (and on the cluster ingress in front of it). The threshold
listener is loopback by default; production exposure happens via the
KMS-facing ZAP server or the gateway.

There is no separate thresholdd daemon to deploy. The luxfi/threshold
repo's cmd/thresholdd remains as a developer / CI binary that imports
the same package — one wire, one implementation, two startup paths.

Tests: TestEmbeddedDispatcher_BLSRoundTrip (full keygen → sign →
verify → forgery rejection) and TestEmbeddedDispatcher_UnknownScheme
(-32601 method-not-found contract) both pass. README updated with the
new protocol table; full surface documented in
luxfi/threshold/LLM.md "thresholdd — unified JSON-RPC dispatcher".
2026-05-13 15:56:56 -07:00
zeekay c6a0e2d1a6 deps: bump luxfi/crypto v1.19.0 — canonical luxfi/crypto/ipa 2026-05-13 11:55:40 -07:00
zeekay 4bf07ef07f client: spell out org-scoping requirements for Threshold.Cancel
Cancel was documented as "cancels a pending session belonging to the
caller's org" but the contract did not name HOW the implementation
enforces that. A loose implementation could:

  - read session, transition state, then check org_id (TOCTOU window)
  - distinguish "not yours" from "not found" in the error message
    (enumeration oracle leaks the existence of cross-org sessions)
  - skip the org predicate when a concurrent legitimate caller also
    has the row open (race condition lets the wrong-org call win)

Tightened doc names the three required behaviors:
  - orgID derived from ctx Claims, not caller-supplied
  - SELECT-FOR-UPDATE + WHERE org_id = ctx.OrgID; mismatch returns
    ErrSessionNotFound (same opaque response as unknown id)
  - row-level CAS with the org predicate in WHERE, so two callers
    cannot race the state transition
  - idempotency on already-cancelled scoped to the SAME org

go.work bumped to 1.26.3 to satisfy the per-module go directive.
2026-05-13 02:47:05 -07:00
zeekay 5e92895cb6 client: harden Threshold — payload-hash binding, idempotency-key conflict, typed errors, ctx-derived identity 2026-05-13 02:18:05 -07:00
zeekay 472c6e2669 client: declare canonical Threshold interface 2026-05-13 01:15:54 -07:00
zeekay 0db2f9b2e5 rename: drop upstream-brand mention in OAuth callback comment 2026-05-13 00:48:41 -07:00
zeekay 81a77675fb go.mod: bump go directive to 1.26.3 (security advisory) 2026-05-12 20:55:00 -07:00
zeekay e1895dc2d8 deps: database v1.18.3 (drops zapdb/v4 transitive) 2026-05-05 17:28:30 -07:00
zeekayandGitHub 00390645b9 fix(build): CGO_ENABLED=1 for go-sqlite3 (unblocks /v1/mpc/wallets) (#4)
* ringtail: fix lux/mpc go.sum corruption (luxfi/edwards25519)

The recorded go.sum hash for github.com/luxfi/[email protected] was
stale — the proxy now serves go.mod content (`module github.com/luxfi/
edwards25519\n\ngo 1.21\n`) that hashes to MBoV+bPE..., while go.sum had
the older tFuuyXvI... value (matching whatever go.mod content was first
published, evidently `module github.com/agl/ed25519`). Result: every Go
build in the workspace, including ringtail's dkg tests, errored with a
checksum mismatch SECURITY ERROR.

Fix: remove the two stale edwards25519 lines and re-run go mod tidy so
the current proxy content is re-pinned. New hashes:

  h1:YPoT831TZMslvNyy/KuTtpHi4BGZWgpwak06RCaYLzo=        (zip)
  h1:MBoV+bPEz1tSMADPMSUaYdZ+agY3syG8B6z7RmJBH0A=        (mod)

Verified: `cd ~/work/lux/ringtail && go test ./dkg/...` passes
TestDKG_2of3, TestDKG_3of5, TestDKG_InvalidParams, TestDKG_MissingData
(env override GOENV=/dev/null + GOPROXY=https://proxy.golang.org,direct
to bypass the unrelated GONOPROXY=hanzoai/* setting in the user's global
go env that prevents the proxy from serving the cached but git-deleted
hanzoai/[email protected] — that's a separate workspace-config issue,
not a go.sum corruption).

* fix(build): enable CGO so go-sqlite3 driver registers

The /v1/mpc/wallets HTTP API was 503'ing because CGO_ENABLED=0
left mattn/go-sqlite3 unregistered, so the wallet tables couldn't
open. Workaround was seeding wallets into TA's user_wallets table.

Switch to CGO=1 with statically-linked external linker so the binary
still runs in a scratch/alpine runtime without sqlite-libs at runtime.
2026-05-05 03:01:20 -07:00
zeekay 08c09c0349 feat(zapauth): JWKS-bearer gate on KMS-facing ZAP server (LP-103)
New pkg/zapauth verifies a JWT presented in OpAuthHello (0x00EF)
BEFORE the X25519+ML-KEM-768 hybrid handshake. Per-peer claims are
attached by ZAP peer ID and consulted on every opcode dispatch.

Backwards compatible: defaults to auth=off so v1.13.x clients keep
working. ZAP_AUTH_REQUIRED=true rejects unauthenticated peers; the
flip happens after every KMS deploys with bearer minting (luxfi/kms
v1.9.0 pkg/iamclient).

- pkg/zapauth/{verifier,verifier_test}.go — Verifier, JWKS cache,
  AuthHello frame, stdlib-only signature verification (RS/ES/EdDSA).
- pkg/api/zap_kms_server.go — KMSZapConfig, StartKMSZAPWith, per-op
  checkAuth gate, OpAuthHello handler.
- pkg/api/zap_kms_server_auth_test.go — required+advisory+missing+bad
  token + nil-verifier integration tests against a self-contained
  ES256 JWKS endpoint.
- cmd/mpcd/main.go — wires ZAP_JWKS_URL / ZAP_EXPECTED_ISS /
  ZAP_EXPECTED_AUDIENCES / ZAP_AUTH_REQUIRED env vars.
- docs/zap-auth.md, LLM.md — protocol reference + LLM map entry.

Tests: pkg/zapauth (13/13), pkg/api KMSZAP (10/10 — 5 original +
5 new auth integration), pkg/zap (existing untouched).
2026-05-02 09:30:48 -07:00
zeekay f9ae303ec9 fix(ui): remove orphan pages blocking docker build
Users/Webhooks/Whitelist/APIKeys/Devices/Login/Operations and the
webauthn.ts shim referenced API methods, hooks, and types that were
never implemented in lib/api.ts or hooks/use-mpc.ts. None are routed
in App.tsx — pure dead code that broke `pnpm build` (and therefore
the Docker UI stage).

Routed pages remain: Dashboard, Wallets, Vaults, Policies,
Settlements, Bridge, Payments, Audit, Settings.

Unblocks ghcr.io/luxfi/mpc:v1.13.2 publish for #70 ZAP rollout.
2026-05-01 01:55:28 -07:00
zeekay e8e1712275 fix(deps): refresh [email protected] go.sum to proxy.golang.org authoritative
CI Docker workflow has been failing across v1.10.x-v1.13.0 with go.sum
checksum mismatch on github.com/luxfi/[email protected].

Root cause: the v0.1.0 git tag was rewritten upstream after publication.
- proxy.golang.org has the original commit dff0e9e2 (module path:
  github.com/agl/ed25519, satisfying replace directive) -> h1:tFuu...
- live git tag now points to eee5e2c2 (module path: luxfi/edwards25519)
  -> h1:MBoV... which is what was stale in go.sum.

CI hits proxy.golang.org by default and gets tFuu..., mismatching the
stale MBoV... in go.sum. sum.golang.org records confirm tFuu... as the
authoritative hash.

Fix: rewrite both go.sum lines for luxfi/[email protected] to the
proxy.golang.org / sum.golang.org authoritative values. No code change,
no go.mod change.

Verified locally with GOPRIVATE temporarily cleared:
  GOPROXY=https://proxy.golang.org GOSUMDB=sum.golang.org go mod download
  go build ./...  (clean)
  go vet ./...     (clean)
  go test -short ./pkg/webauthn/... ./pkg/db/...  (pass)
2026-05-01 01:46:07 -07:00
zeekay 4b30830afc fix(mpcd): default --kms-zap-listen to :9653 to match operator zapPort
The Liquid operator already provisions a Service exposing port 9653
on every MPC StatefulSet (spec.zapPort=9653). Match that default so
no operator/CR change is required for v1.13.0 rollout.
2026-05-01 01:36:46 -07:00
zeekay 3cf1004d92 feat(mpcd): expose ZAP server on :9970 for KMS handshake
Adds threshold-signing transport so KMS can use mpcd as MPC backend.
Reuses ML-KEM-768 hybrid handshake from luxfi/kms/pkg/zap (#55) by
vendoring the same construction into mpc/pkg/zap — wire constants
(opcodes 0x00F0/0x00F1, HKDF info "kms/zap/v1/session", direction
tags) stay in lockstep with KMS.

New surface:
- pkg/zap: vendored handshake + session (forward-only convergent)
- pkg/api/zap_kms_server.go: KMS-facing ZAP node (opcodes
  0x0001/0x0010/0x0011/0x0012/0x0020) wired onto MPCBackend
- cmd/mpcd: --kms-zap-listen flag (default :9970), enabled out of the
  box; default-disable by passing empty string

Existing pkg/api/zap_server.go (dashboard event bus, opcodes 0x0060+)
is unchanged — different consumer, different port.

Closes the secrets-only mode gap on mainnet (#70).
2026-05-01 01:35:13 -07:00
zeekay a80c75b0c6 fix(cc/attest): TDX/NRAS stubs error-return instead of panic
TDX.Verify and NRAS.Verify panicked when reached during stage 1 of
issue #222, taking the whole MPC process down whenever Dispatch was
called from a request-handling goroutine. The fail-loud guarantee is
preserved by returning ErrNotImplemented (a hard refusal — callers
must not paper over it) so the release gate still refuses, but the
process survives. Updates verifier_test.go's stub coverage from
TestStubsPanic to TestStubsReturnNotImplemented and adds Dispatch-path
coverage for the same behaviour.
2026-04-29 00:51:02 -07:00
zeekay 6ea2a7427d chore: bump deps (lattice v7.1.0, hsm v1.1.3, sqlite3 v1.14.42)
- lattice v7.0.2 -> v7.1.0: types package (ComputeTrustMode,
  ConfidentialIOLevel, WorkloadPrivacyClass) used by pkg/registry +
  pkg/attestation. Was on feat/lp-137-types branch; merged + tagged.
- luxfi/hsm v1.1.2 -> v1.1.3: airgap factory cases (coldcard,
  foundation, keystone, ngrave, gridplus, ledger, trezor, yubihsm,
  nitrokey, pkcs11). Unblocks TestAirgapProvidersRoundTripFactory.
- mattn/go-sqlite3 v2.0.3+incompatible -> v1.14.42: json1 default-on,
  fixes pkg/custody json_extract failures (no build-tag dance needed).
- pkg/approval/factory_test.go: yubihsm now wantError=false (v1.1.3
  exposes it), removes the v1.1.2 workaround comment.
- cc/attest/verifier_test.go + pkg/hsm/hsm_test.go: pre-existing minor
  test cleanups bundled in (stub-panic and gcp/azure-stub assertions).

Build + 36/36 packages pass. Standalone integration smoke
(cmd/mpcd TestStandaloneSelfQuorum, -tags integration): PASS.
2026-04-29 00:19:15 -07:00
zeekay 6495b89e56 test(mpcd): standalone 1-of-1 integration smoke + cmd/mpcd/README
Document mpcd's two topology modes explicitly in cmd/mpcd/README.md:
standalone (1-of-1 self-loop) for smoke tests / dev sandboxes, and
networked (t-of-n) for production. The same binary handles both — only
flags differ.

Add cmd/mpcd/standalone_test.go behind the integration build tag. It
builds mpcd, runs it with --threshold=1 and no peers, polls /healthz
until signing_quorum=true, and asserts the response shape (mode,
threshold, version). Keygen is intentionally NOT exercised — CGGMP21
and FROST cannot produce shares with a single party.

Run: go test -tags=integration ./cmd/mpcd/... -count=1 -timeout 90s

Honest scope-limit: mpc trunk has a pre-existing build break in
pkg/policy (undefined fhethr.* symbols) and pkg/api (handlers_approval
references s.approval that's not on the Server struct). The smoke test
itself is correct and passes type-check against cmd/mpcd; it will go
green once the upstream policy/api regression is resolved.
2026-04-28 13:42:36 -07:00
zeekay f2f5e93b4b docs(changelog): record v1.11.0 published tag
cc/attest production wire (#222 + N1) + RequireSEVSNP/TDX/NVNRAS strict
policy (D2) + experimental_tfhe gate (N2) + airgap_command +
buildHSMSignerConfig + nonce binding (B4).
2026-04-28 10:04:06 -07:00
zeekay 8e466e8e9d Merge release-policy-require-flags-2026-04-28 2026-04-28 09:30:21 -07:00
zeekay 4e65cb182a Merge fix-buildhsmsignerconfig-2026-04-28 2026-04-28 09:29:02 -07:00
zeekay a3343c4155 Merge fix-edwards25519-sum-2026-04-28 2026-04-28 09:28:22 -07:00
zeekay 0dd165bd2b Merge tfhe-real-threshold-wire-2026-04-28 2026-04-28 09:27:21 -07:00
zeekay d4ed628d74 kms/release: wire cc/attest chain verifier into Release path
Red Final N1 fix. Release() previously called only Attestation.Verify(nonce)
and ReleasePolicy.Permits() before sealing the session key. Neither step
performs the actual SEV-SNP / TDX / NRAS chain verification — AMD KDS
lookup, ARK→ASK→VCEK chain, NRAS JWT — leaving the #222 go-sev-guest
verifier as dead code on the release path. A worker that fabricates a
CompositeAttestation envelope whose Verify(nonce) returns true and whose
RIM/HW/issuer membership matches policy could harvest a session key
without ever presenting AMD-signed evidence.

Wire-in:
  - kms.CompositeAttestation gains VerifyEvidence(ctx, opts...) returning
    []*attest.VerifiedReport. *attestation.CompositeAttestation already
    implements this method (composite.go:320 → attest.Dispatch per blob).
  - ReleaseRequest gains optional Ctx + AttestOptions for context plumbing
    + per-call policy (e.g. attest.WithExpectedReportData(nonce[:])).
  - Release() calls VerifyEvidence between the cheap nonce check and the
    policy permit step. Any chain failure → ErrAttestationChain joined
    with ErrPolicyRefused via errors.Join. No silent acceptance.
  - New sentinel ErrAttestationChain.

Tests (go test -v -short -race -count=1 -timeout 60s ./pkg/kms/...):
  - all 14 existing TestRelease_/TestNonce_/TestAAD_ remain green
  - NEW TestRelease_RefusesOnChainFailure: Verify(nonce)=true but
    VerifyEvidence returns ErrChainInvalid → MUST surface
    ErrAttestationChain wrapped under ErrPolicyRefused
  - NEW TestRelease_AcceptsOnChainPass: paired happy-path so the
    chain-step is the only difference, not a wiring regression

Also pulled in cherry-pick of canonical go-sev-guest sums (baaeca8) so
cc/attest builds reproducibly outside the dirty local cache.
2026-04-28 08:47:09 -07:00
zeekay bc17282283 fix(go.sum): canonical edwards25519 v0.1.0 hashes
Stale luxfi/edwards25519 v0.1.0 sums caused checksum mismatch on every
go build. Replaced with sumdb canonical hashes from proxy.golang.org.

  h1:zuSrBtgV4...  -> h1:YPoT831TZMs... (zip)
  h1:tFuuyXvI+...  -> h1:MBoV+bPEz1t... (go.mod)

Verified via: go mod download -json github.com/luxfi/[email protected]
Same drift class as the luxfi/log + base sum churn from this week.

Tests: 28 packages that previously halted at SECURITY ERROR now
compile/run. Remaining failures (api/client/mpc/policy build, attestation
setup, custody json_extract) are unrelated drift classes for separate
tickets.
2026-04-28 08:46:19 -07:00
zeekay 97e50fd5b6 sec(mpc): gate TFHE wallet keygen behind experimental_tfhe build tag
The threshold-FHE wallet keygen path (pkg/mpc/tfhe_session.go:154,165
production caller via pkg/mpc/node.go:583) was wired to
luxfi/threshold/protocols/tfhe.NewKeyGenerator + GenerateKeys. Per #197,
that primitive is a fail-loud placeholder: every party stores the full
master key, PartialDecrypt returns an HMAC tag, and CombineShares ignores
partials and runs single-party decryption. Upstream guards every entry
point with guardUnsafe() that panics unless
LUX_ALLOW_FAKE_TFHE_FOR_TESTING_ONLY=1 is set — by design.

Default production builds must not be able to reach the placeholder
primitive even with the env var set. Real-threshold wiring (luxfi/lattice
Shamir/LWE) is tracked as a multi-week separate task per LP-137 §2.6.

Changes:

- pkg/mpc/tfhe_session.go: //go:build experimental_tfhe — entire
  tfheKeygenSession + tfheComputeSession + TFHESession interface gated
  behind the tag.
- pkg/mpc/tfhe_node.go (new, experimental_tfhe): Node.CreateTFHEKeyGenSession
  + CreateTFHEComputeSession constructing the experimental sessions.
- pkg/mpc/tfhe_node_stub.go (new, !experimental_tfhe): stub Node methods
  that return ErrTFHENotImplemented from every entry point. This is what
  the production binary ships with.
- pkg/mpc/tfhe_errors.go (new, unconditional): ErrTFHENotImplemented
  symbol with a description pointing at LP-137 §2.6.
- pkg/mpc/node.go: removed the unconditional CreateTFHEKeyGenSession +
  CreateTFHEComputeSession definitions and the unused luxfi/fhe import.

Test pass:

  go test ./pkg/mpc/... ./pkg/policy/... -short -count=1 -race -timeout 60s
  ok  github.com/luxfi/mpc/pkg/mpc      2.016s
  ok  github.com/luxfi/mpc/pkg/policy   1.477s
  91 PASS, 0 FAIL (default build, no env var set)

  go build -o /tmp/mpcd_final ./cmd/mpcd/
  /tmp/mpcd_final version  # mpcd version 0.3.3 — no panic, no UNSAFE TFHE reach

  go build -tags experimental_tfhe ./pkg/mpc/...  # opt-in build OK

Note: pkg/policy/main_test.go sets LUX_ALLOW_FAKE_TFHE_FOR_TESTING_ONLY=1
because RealThresholdDecryptor's dispatcher tests exercise the placeholder
PartialDecrypter — same TestMain pattern threshold/protocols/tfhe uses.
These are orchestration-shape tests; they verify no threshold security
property because the underlying scheme has none.

Companion bumps required for the branch to compile:
- luxfi/threshold v1.6.0 -> v1.6.4 (FHECiphertext / FHEThresholdShare /
  ShareAggregator exports landed in v1.6.x)
- google/go-sev-guest -> go.mod (cc/attest/sev.go direct import)
- pkg/api/server.go: missing approval + tieredWallets fields added to
  satisfy in-tree handler references
2026-04-28 08:46:08 -07:00
zeekay 95c9dd08f8 kms/release: add Require* flags mirroring AttestationBaseline (#203 O5)
Per LP-137 §2.7, ReleasePolicy now exposes RequireSEVSNP / RequireTDX /
RequireNVNRAS boolean flags matching the C++ AttestationBaseline.require_*
pattern. Closes the asymmetry where the C++ side enforced explicit issuer
presence but the Go side only checked RIM + AllowedHardware membership.

Permits() default-deny: if a flag is true and the corresponding canonical
issuer string ("amd.sev.snp", "intel.tdx", "nvidia.nras.v1") is absent
from att.EvidenceIssuers(), the policy refuses release.

NewReleasePolicyStrict bundles the production posture (all three flags
true) so deployments cannot accidentally ship a wide-open policy. Tests
opt out via NewReleasePolicy + leaving the flags false.

Interface change: CompositeAttestation gains EvidenceIssuers() []string.
Wire-stable strings exported as IssuerSEVSNP / IssuerTDX / IssuerNVNRAS
matching cc/attest dispatch keys.

Tests: 4 new cases - happy path (all 3 issuers present + valid),
missing SEV-SNP, missing TDX, missing NVNRAS (incl. nil-issuer edge).
All 19 pkg/kms tests pass with -race -count=1 -short -timeout 60s.
2026-04-28 08:37:19 -07:00
zeekay f68c8038d9 fix(go.sum): canonical edwards25519 v0.1.0 hashes
Stale luxfi/edwards25519 v0.1.0 sums caused checksum mismatch on every
go build. Replaced with sumdb canonical hashes from proxy.golang.org.

  h1:zuSrBtgV4...  -> h1:YPoT831TZMs... (zip)
  h1:tFuuyXvI+...  -> h1:MBoV+bPEz1t... (go.mod)

Verified via: go mod download -json github.com/luxfi/[email protected]
Same drift class as the luxfi/log + base sum churn from this week.

Tests: 28 packages that previously halted at SECURITY ERROR now
compile/run. Remaining failures (api/client/mpc/policy build, attestation
setup, custody json_extract) are unrelated drift classes for separate
tickets.
2026-04-28 02:50:01 -07:00
zeekay 39fcdca875 mpcd: author missing buildHSMSignerConfig helper (refs #227)
Commit af4139d ("deployment-mode flag + audit dispatcher") referenced
buildHSMSignerConfig at the HSM signer wiring site but the helper file
was never staged — same pattern as the missing airgap files repaired
under #208. The current main.go works around the gap by passing nil to
hsm.NewSigner, which loses the operator-supplied --hsm-signer-key-id and
prevents per-provider config promotion.

cmd/mpcd/hsm_signer_config.go:
  - validates provider name against the luxfi/hsm v1.1.3 factory list
    (aws/gcp/azure, zymbit, yubihsm, nitrokey, pkcs11, kmip, coldcard,
    foundation, keystone, ngrave, gridplus/lattice, ledger, trezor,
    tr31, mldsa, local) and the documented aliases.
  - promotes --hsm-signer-key-id into the canonical config key for each
    cloud KMS provider (aws.key_id, gcp.crypto_key, azure.key_name).
  - leaves vendor-specific surface (slot/pin/library, vault URL,
    transport) to the factory's MPC_HSM_<P>_* env-var fallback so the
    CLI flag surface stays minimal.

cmd/mpcd/main.go: route --hsm-signer through the helper and surface
config errors before reaching hsm.NewSigner.

cmd/mpcd/hsm_signer_config_test.go: covers cloud-KMS key-id promotion,
hardware-wallet empty-map case, alias acceptance, case-insensitive
provider names, and rejection of empty + unknown providers. Helper
+ tests pass under `go test -race -count=1` in isolation.

go.sum: refresh github.com/luxfi/edwards25519 v0.1.0 hashes to match
proxy.golang.org. Pre-existing checksum drift blocked all `go build`
attempts on this branch and on main.

Note: cmd/mpcd as a whole still does not compile on this branch — the
same af4139d commit also dropped pkg/api/handlers_approval.go without
adding the Server.approval field, and pkg/policy/fhe_threshold_decryptor.go
references symbols that never landed. Both are out of scope for #227 and
will be repaired under follow-up tickets in the same #208-style cleanup.
2026-04-28 01:14:50 -07:00
zeekay 0a8fa2e0cc Merge kms-nonce-bind-2026-04-28
# Conflicts:
#	CHANGELOG.md
2026-04-28 00:58:33 -07:00
zeekay 48744d2e82 cc/attest: canonical TEE attestation verifier (#222 stage 1)
Adds the canonical TEE attestation verifier package at cc/attest. This
is the home for chain validation + trust anchor management; the
luxcpp/crypto/attestation parser primitives feed bytes here BEFORE
their measurement output is trusted.

Stage 1 — SEV-SNP — PRODUCTION:
  - cc/attest/sev.go: SEVSNP{} verifier built on
    github.com/google/go-sev-guest v0.14.1. Live VCEK fetch from AMD
    KDS, ARK→ASK→VCEK chain validation, signature verify against the
    leaf, MEASUREMENT/REPORT_DATA/HOST_DATA/CHIP_ID extraction,
    caller-supplied policy gates (WithExpectedReportData,
    WithExpectedMeasurement). 197 LOC.
  - testdata/: real-vendor Milan attestation report + VCEK leaf
    committed verbatim from the upstream go-sev-guest test corpus.
    Tests replay the AMD KDS responses via SimpleGetter so CI never
    hits the network.
  - 9 SEV-SNP tests passing (happy path, dispatch, length reject,
    tampered signature, wrong report_data, wrong measurement,
    deterministic composite hash, unknown kind, stub-panic guard).

Stage 2 — TDX — STUB (#222 stage 2):
  - cc/attest/tdx.go: TDX{}.Verify panics with ErrNotImplemented so
    any caller routing TDX evidence during stage 1 fails LOUD. Plan
    documented inline: go-tdx-guest + Intel PCS + QE/TDX module
    identity pinning.

Stage 3 — NRAS — STUB (#222 stage 3):
  - cc/attest/nras.go: NRAS{}.Verify panics with ErrNotImplemented.
    Plan: lift the existing pkg/attestation/nvidia NRAS client into
    the cc/attest contract with a JWKS cache.

Wire-in:
  - pkg/attestation/composite.go gains VerifyEvidence(ctx, opts...)
    which dispatches each Evidence blob to cc/attest by Issuer.
    Callers (kms release gate, scheduler) MUST invoke this before
    accepting parser output.

Doc:
  - cc/attest/doc.go states the production/stub matrix and the
    layering contract between the C-ABI parser and this package.
2026-04-28 00:47:37 -07:00
zeekay aab0b7e0d1 docs: CHANGELOG narrating Dec 2025 implementation timeline
Original implementation completed by 2025-12-25. Source tree was lost
in a laptop-theft data-loss event in early 2026. Re-published 2026-04-28
from memory and audit recovery.

This CHANGELOG narrates the actual implementation order with full-SHA
citations into the re-published commits, so a reader of git log can
understand what came first/next/etc. without any commit timestamps
being rewritten.

Phases covered: KMS release nonce binding (HMAC-SHA256 with durable
NonceStore + 7 regression tests), NRAS JWS hardening (alg x key-type
strict binding, alg=none rejection), NRAS SkipSignature kill-switch
deletion, mpcd air-gap recovery (Coldcard / Foundation BBQr /
Keystone+NGRAVE UR), and the luxfi/hsm v1.1.3 dependency bump.
2026-04-28 00:01:45 -07:00
zeekay abb64892eb kms/release: add F2/B4 nonce-binding regression suite
Five test cases asserting the audit-required nonce binding properties:

  1. Round-trip — Issue() → Release(returnedNonce) → success
  2. Wrong nonce — forged nonce ≠ issued → ErrNonceMismatch
  3. Cross-job replay — Issue(A,X) → Release(B,X) → ErrNonceMismatch
  4. Cross-epoch replay — Issue(N,A) → Rotate → Release(N+1,A) refused
  5. Double-Release — Issue → Release → Release → ErrAlreadyConsumed

All five PASS under -race. Pinned to the d5d685c binding fix.
2026-04-27 21:36:14 -07:00
zeekay 83d7428718 kms/release: bind nonce to (epoch, jobID) and persist replay state
Closes Blue audit B4. Four nonce-binding gaps in the ReleaseGate are
closed in one pass:

1. Nonce derivation is now HMAC-SHA256(rootKey, "kms/release/v1" ||
   epoch || jobID || rand32). A nonce issued for jobID A cannot be
   replayed under jobID B even if the random component is identical.

2. Issue() persists (jobID, nonce, epoch, expires) in a NonceStore.
   Release() looks up the stored nonce and constant-time-compares it
   against req.Nonce. The forged-nonce path now refuses with
   ErrNonceMismatch instead of silently accepting any 32 bytes.

3. AAD now binds the issued nonce: epoch || jobID || teePub || nonce.
   Two Release() calls with the same (epoch, jobID) but different
   issuances produce distinct AADs; sealed keys are non-fungible
   across issuances even after a process restart.

4. State (epoch + pending nonces + consumed-set) is durable via the
   NonceStore interface. Two implementations:
   - MemoryNonceStore for tests / single-shot dev
   - DatabaseNonceStore for prod (luxfi/database; pair with encdb +
     ZapDB for at-rest encryption per Hanzo Base pattern)

Replay window default 1h; pending TTL 1h. GC reclaims both.

Tests cover all five required scenarios + nonce-binding + AAD-binding
asserts. -race clean.

Backwards-incompatible change to NewLocalReleaseGate / NewMPCReleaseGate
/ NewHSMReleaseGate signatures; no compat shim per audit guidance.
2026-04-27 21:33:04 -07:00
zeekay 48bc23c225 k8s: probe /healthz (v1.10.0 renamed /health → /healthz)
The internal API now exposes /healthz returning JSON readiness state.
/health returns 404.
2026-04-25 21:50:50 -07:00
zeekay 3686dcbdd3 k8s: rename env LUX_MPC_PASSWORD → MPC_PASSWORD (v1.10.0)
HSM provider reads MPC_PASSWORD by default. Legacy LUX_MPC_PASSWORD
was the old prefix-style env name.
2026-04-25 21:47:54 -07:00
zeekay dae0f9258d neutral: drop 'Lux'/'Hanzo' brand prefixes in user-facing strings
MPC and KMS are protocols, not products. The Go module path
github.com/luxfi/mpc remains canonical (location), but user-facing
strings (--help, README, comments, banners) are brand-neutral.
'Lux MPC' → 'MPC', 'Lux KMS' → 'KMS', 'Hanzo MPC' → 'MPC'.
2026-04-25 21:46:16 -07:00
zeekay 9ccc92403b k8s: drop --mode consensus, --api-db, --api-kv (v1.10.0 removed these)
mpcd v1.10.0 supports only consensus mode (legacy NATS+Consul removed).
Dashboard API moved to separate mpc-api deployment.
2026-04-25 21:43:49 -07:00
zeekay c471fad207 k8s: pin StatefulSet to ghcr.io/luxfi/mpc:v1.10.0 (was mpc-api:latest)
luxfi/mpc CI publishes ghcr.io/luxfi/mpc; the legacy mpc-api image
is gone. Pinning the SHA-tagged release ensures rollouts are
deterministic (matches operator pattern).
2026-04-25 21:27:47 -07:00
zeekay 0d7bd2c0e4 feat: ZAP=9999 across remaining mpc deployments + Dockerfiles
Sweeps the files missed in the earlier commit:
- top-level Dockerfile (EXPOSE)
- minio/lux-mpc-builds/Dockerfile-iam (EXPOSE)
- deployments/bridge/compose.yml (peer addresses + listen)
2026-04-25 21:21:02 -07:00
zeekay 0b8b6fdc0c feat(zap): adopt canonical port 9999 across MPC + transport
ZAP is the binary protocol — like HTTP=80 and HTTPS=443, ZAP=9999
across the Lux ecosystem. The DNS name (e.g. zap.kms.svc, zap.mpc.svc,
mpc-node-headless.lux-mpc.svc) disambiguates the service; the port is
always the same.

Changes:
- mpcd --listen default :9651 → :9999
- pkg/transport/{transport,factory}.go: comments and examples
- k8s/mpc-statefulset.yaml: ports + peer template
- k8s/network-policy.yaml: ingress/egress allow rules
- cloudbuild.yaml: EXPOSE
- deployments/customer-3of5/{compose.yml,k8s/*,topology.yaml}: full
  template sweep — every peer hostname is distinct, port is uniform 9999
- README.md / LLM.md: example commands

luxd validator P2P (9631 mainnet, 9641 testnet, 9651 devnet) is a
DIFFERENT protocol on a DIFFERENT pod and is unchanged.
2026-04-25 21:18:39 -07:00
c77caa9332 ci: migrate runner from lux-build to hanzo-build (ARC consolidation) (#3)
Consolidating to single canonical ARC fleet. Killing duplicate lux-build
scale set. Hanzo ARC provides identical capacity (max 30 on DO + max 30
ARM on GKE T2A) across hanzo/lux/zoo/liquidity repos — one and only one
way to run self-hosted CI.

No behavior change for this workflow.

Co-authored-by: Hanzo Dev <[email protected]>
2026-04-23 18:01:20 -07:00
zeekay 674787ba91 deps: bump luxfi/hsm v1.1.1 → v1.1.2
v1.1.2 renames EnvProvider default env var from
LUX_MPC_PASSWORD to MPC_PASSWORD. The binary now reads the
canonical name used by the K8s operator injection.
2026-04-22 03:53:56 -07:00
zeekay 0a20ec7bf6 fix(mpc): HasSigningQuorum uses operator-facing threshold semantics
Follow-up to 8156f2e. The CLI/CRD `--threshold` flag surfaces the
OPERATOR-facing "minimum signers required" count (e.g. threshold=2 for
a 2-of-3 ensemble, threshold=3 for 3-of-5). The CGGMP21 signing API
internally stores the polynomial degree (one below signers-required)
in keyInfo.Threshold — so the sign code continues to compare
`len(signerPeerIDs) >= keyInfo.Threshold + 1`. Those two surfaces are
off-by-one by protocol library convention.

HasSigningQuorum is operator-facing (used by /healthz) so it uses the
CRD/CLI semantics directly: quorum holds when `readyCount >= threshold`.

Scientist's repro: 3-node 2-of-3 ensemble (CRD threshold=2) with one
peer down — readyCount=2, threshold=2 → quorum holds, /healthz returns
200. With the previous commit the arithmetic was readyCount>=threshold+1
which would have required all 3 nodes up.

Tests updated accordingly (Test_HasSigningQuorum_3Node_2of3 now uses
threshold=2 matching the deployed CRD, Test_HasSigningQuorum_5Node_3of5
uses threshold=3, boundary test covers the new arithmetic).
2026-04-20 18:52:49 -07:00
zeekay 8156f2ed4e fix(mpc): /healthz reports quorum — not all-peers-ready (P0-1)
Survivor nodes in a t-of-n ensemble now report healthy so long as they
still have enough ready peers (including self) to form a signing quorum
for their configured threshold. Previously, ArePeersReady() was the gate
and any single peer loss collapsed availability on the remaining nodes.

Repro (Scientist, 2026-04-20): scale liquid-mpc-mpc-node StatefulSet
from 3 → 2 replicas in the liquidity devnet; surviving pods returned HTTP
503 with log "Peers not ready expected=3 ready=2" even though 2 nodes
are sufficient for the 2-of-3 (threshold=1) signing quorum.

Fix:

- pkg/transport/registry.go: add HasSigningQuorum(threshold) which returns
  true when readyCount >= threshold+1 (the CGGMP21/FROST signers-required
  count used everywhere in pkg/mpc/node.go).
- pkg/mpc/registry.go: matching method on the legacy Consul-backed
  registry plus interface addition to PeerRegistry.
- cmd/mpcd/main.go: /healthz handler switched to HasSigningQuorum(threshold)
  for the 200/503 gate. ArePeersReady() is still reported in the JSON
  body (as `ready`) for observability. `status` becomes
  "healthy" / "healthy-reduced" / "degraded" so operators can see the
  in-between state.
- cmd/mpcd/main.go: ConsensusPeerRegistry wrapper delegates the new
  method to the underlying transport registry.

Keygen sessions deliberately continue to require ArePeersReady() — a
fresh wallet still needs every participant present. Only the /healthz
gate is relaxed, which matches the availability story for signing.

Tests: pkg/transport/registry_test.go covers
- Test_HasSigningQuorum_3Node_2of3 (the Scientist repro — survivors
  still quorum-ok after one peer loss)
- Test_HasSigningQuorum_5Node_3of5 (larger ensemble, f=2 tolerance)
- Test_HasSigningQuorum_ArePeersReady_Decoupled (the two APIs do not
  gate each other)
- Test_HasSigningQuorum_BoundaryCheck (pins arithmetic: required =
  threshold+1, not threshold)

All 11 test cases pass; transport + mpc package suites clean.
2026-04-20 18:48:31 -07:00
zeekay 1f926f1e20 fix(ui): gate //go:embed behind embedui build tag
CI lint was failing because ui/dist/ doesn't exist in a clean checkout.
Default build compiles with no embedded assets. Production builds that
want the bundled dashboard must build with -tags embedui after
`pnpm --dir ui build`.
2026-04-18 23:37:53 -07:00