Commit Graph
218 Commits
Author SHA1 Message Date
zeekay 1dc9a9e6f2 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.
2025-12-28 15:07:49 -08:00
zeekay 0d917ec271 fix(mpc): HasSigningQuorum uses operator-facing threshold semantics
Follow-up to 5215d42. 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).
2025-12-28 13:02:00 -08:00
zeekay 5215d4275d 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.
2025-12-28 12:55:23 -08:00
zeekay 60f9bd7ce1 feat(mpc): R6 requireRegulator invariant test + T3 CodeQL/SBOM workflow fix
R6 — handlers_treasury_test.go covers the `requireRegulator=true` tier
invariant: when numeric quorum is reached WITHOUT the regulator on a tier
that requires them, handleTreasurySign must return 409 and revert the op
to pending_approval. Tests cover the reject path, the happy-path
finalization with regulator, the control case with requireRegulator=false,
and the create-time validator's refusal of regulator-required tiers on
wallets with no regulator shard.

T3 — ci.yml references `cmd/lux-mpc-cli` in three places (CodeQL build,
SBOM build, and post-CI build). That directory was renamed to `cmd/mpc` in
adaa38b but the workflow was never updated, so CodeQL/SBOM have been
failing on a missing path. Fix all three references to `./cmd/mpc`.

Run: CGO_ENABLED=1 go test -tags json1 -run TestR6 -v ./pkg/api/
2025-12-28 12:02:24 -08:00
zeekay 324b88fa8b feat(mpc): treasury 3-of-5 + purge Postgres, SQLite-only
- Postgres purge: delete cas_postgres_test.go, strip PG sidecar from CI
- Treasury 3-of-5 governance: pkg/api/handlers_treasury.go + models
- Routes: POST /v1/mpc/treasury/wallets, POST /v1/mpc/treasury/sign,
  GET /v1/mpc/treasury/wallets/{id}
- Per-org HSM isolation via tenant-scoped cosigner key
- Threshold escalation: small=3-of-5, large=4-of-5
2025-12-28 11:29:17 -08:00
zeekay b8846bcd0b fix(webauthn): F4 LAX tightening — credentialHash floor + both-must-match
Red round 4 flagged (F4, MEDIUM-HIGH) that the previous LAX binding
mode in VerifyLiveness permitted a cross-ceremony replay: an attacker
who observed a fresh WebAuthn challenge could trick SecureGate into
signing a liveness envelope binding to the challenge alone (no
credentialHash), then replay that envelope against any enrollment for
the same userId that happened to reuse the challengeId-only
expectation.

Two LAX-only tightenings in pkg/webauthn/liveness.go (STRICT already
enforced the missing-both rejection; this sharpens LAX):

1. credentialHash is a hard floor. If the server expects
   credentialHash and the envelope omits it, reject with
   "credentialHash required in LAX mode — envelope without
   credentialHash is replay-vulnerable". challengeId alone cannot
   satisfy the binding requirement in LAX — challengeId can be
   observed and replayed; credentialHash commits to the specific
   pubKey being enrolled.

2. When the envelope supplies BOTH credentialHash AND challengeId AND
   the server expects both, require BOTH to match. No more "either-or"
   loophole. The existing mismatch-rejects guards already caught
   individual mismatches; this closes the pathological
   both-supplied-one-blank-expected corner.

handleBiometricEnroll always commits ExpectedCredentialHash (derived
from the enrollment pubKey via sha256), so the LAX floor always
engages in practice for WebAuthn enrollment — this hardens what
is already the exercised path.

Test coverage (pkg/webauthn/liveness_test.go):

- TestLiveness_R38_LaxRejectsMissingCredentialHash — primary F4 gate:
  envelope with only challengeId, missing credentialHash → reject
- TestLiveness_R38_LaxAcceptsBothMatch — happy path (extended envelope
  after SecureGate rollout)
- TestLiveness_R38_LaxRejectsMismatchedCredentialHashBothPresent
- TestLiveness_R38_LaxRejectsMismatchedChallengeIDBothPresent
- TestLiveness_R38_LaxAcceptsCredentialHashOnly — current SecureGate
  envelope shape still accepted
- TestLiveness_R38_LaxRejectsMissingBindingWhenCredentialHashExpected —
  replaces the old lax-warns-missing path
- TestLiveness_R38_LaxWarnsOnMissing_ChallengeIDOnly — warn path still
  available when credentialHash is not expected

Full pkg/webauthn suite green (18/18). pkg/api regression tests pass.
pkg/custody pre-existing failures unrelated to this change (SQLite
json_extract missing, blame predates F4).

LLM.md entry R35 documents the full set. Cut v1.7.1 on merge.
2025-12-28 11:22:40 -08:00
zeekay 7c4829295d R3-9: unify consumeBiometricChallenge on READ COMMITTED + GetForUpdate
consumeBiometricChallenge was the odd one out: it used SERIALIZABLE
while the two other row-local CAS sites (consumeSessionForSign and
tryApproveOperation) used READ COMMITTED + GetForUpdate. For row-
local CAS the FOR UPDATE row lock already gives us the atomicity we
need; SERIALIZABLE adds cost and forces waiters to abort with 40001
instead of blocking. One pattern, one way.

Aligns with LLM.md's R2-2 note (READ COMMITTED + FOR UPDATE is the
canonical row-local CAS pattern for this repo).
2025-12-28 11:09:25 -08:00
zeekay ab1082567f R3-8: bind liveness envelope to WebAuthn credential + challenge
A stolen SecureGate attestation (within its 2-minute validity window)
could be replayed against a different WebAuthn enrollment because the
envelope only attested (userID, score, timestamp) — it said nothing
about WHICH public key the user was actually enrolling.

Fix: extend LivenessAttestation with two optional binding fields:

  credentialHash = sha256(publicKey) [base64-std]
  challengeId    = the WebAuthn challenge being enrolled [base64url]

handleBiometricEnroll computes both values from the real enrollment
data (consumed challenge bytes + submitted public key) and passes them
as ExpectedCredentialHash / ExpectedChallengeID to VerifyLiveness.
Mismatches are always fatal. A missing binding is fatal in STRICT
mode (the default) and warns-and-accepts in LAX.

BindingMode is server-configured via MPC_LIVENESS_BINDING=strict|lax.
Production mainnet runs STRICT — SecureGate must ship the extended
envelope as a deployment blocker. LAX exists solely for the rollout
window; every permissive accept emits an `mpc.biometric.binding_warn`
log line so operators can see rollout progress.

Tests added in pkg/webauthn/liveness_test.go:
  - R38_AcceptsMatchingCredentialHash
  - R38_RejectsMismatchedCredentialHash
  - R38_RejectsMissingBindingStrict
  - R38_LaxWarnsOnMissing
  - R38_AcceptsMatchingChallengeID
  - R38_RejectsMismatchedChallengeID

Full ./pkg/api/ suite green; the existing pre-R3-8 LAX-style liveness
tests still pass (they supply no Expected* values → the binding path
is a no-op for them).
2025-12-28 11:02:48 -08:00
zeekay ae14ae6a56 R3-2: remove * wildcard bypass in API-key hasPermission
A misissued API key with permissions=["*"] was acting as a god-key,
passing every named permission gate including mpc:settlement:sign,
mpc:operations:approve, mpc:wallet:sweep, and mpc:policy:write.

Every API key must now enumerate its explicit permissions. This is the
one-way-to-express-authority rule: the list is the source of truth —
no wildcards, no implicit grants.

Tests:
- TestRequireRoleOrAPIPermission updated: `*` is rejected on every check
- New TestRequireRoleOrAPIPermission_WildcardRejectedOnSensitive: table
  over every sensitive permission, asserts wildcard is forbidden
- New TestRequirePermission_WildcardRejected: same rule on the lower
  gate used by trade approval

Breaking change: any operator who issued keys with permissions=["*"]
must reissue with explicit permissions. v1.7.0.
2025-12-28 10:56:11 -08:00
zeekay cc52b14802 fix(mpc): R2-1/R2-2 — webauthn verification + CAS under Postgres
R2-1: biometric enrollment is a forged-enrollment oracle fixed.
- pkg/webauthn/verify.go: full WebAuthn verification — compare against the
  random challenge bytes we issued (not the DB row ID), origin allowlist,
  rpIDHash equality, UP+UV authenticator-flag enforcement, ceremony type.
- pkg/webauthn/liveness.go: SecureGate-signed liveness attestation. Ed25519
  envelope over {providerId, userId, score, timestamp, nonce}. MPC verifies
  against MPC_SECUREGATE_PUBKEY_ED25519 and enforces userId match, score
  floor, max age. Body-supplied livenessScore is never trusted again.
- If SecureGate pubkey is unconfigured, handleBiometricEnroll returns 503.
- 14 new tests cover each rejection path.

R2-2: CAS-under-Postgres. The previous in-Go check-then-write +
unconditional UPSERT works on SQLite (writeMu) but loses updates under
Postgres READ COMMITTED. Switched to SELECT ... FOR UPDATE inside READ
COMMITTED tx via orm.RunInTransactionWith + orm.GetForUpdate[T]. Requires
hanzoai/[email protected] (tagged today).
- Session consume: operationLimit strictly respected under concurrency;
  valueAccum computed under the lock.
- Operation approve: ApprovedBy append under the row lock, quorum
  finalization transitions at most once even under racing approvers.
- pkg/db.New now supports postgres:// DSNs.
- New Postgres-backed regression tests in cas_postgres_test.go —
  auto-skipped when no TEST_PG_DSN reachable; 10 concurrent consumers
  against operationLimit=5 yields exactly 5 successes and 5 denials.

Bumps github.com/hanzoai/orm v0.3.2 → v0.4.0.

Test plan:
  go test -tags sqlite_json1 -count=10 \
    -run 'CASUnderRace|CASUnderRace_Postgres|RequireRoleOrAPIPermission' \
    ./pkg/api/ ./pkg/webauthn/
2025-12-28 10:49:33 -08:00
zeekay 3a9e341723 remove: "api" role pass-through from requireRole — one way only
Every /v1 mutating route that previously accepted role=="api" without
permission checks now uses requireRoleOrAPIPermission — JWT human roles OR
API keys that carry the concrete permission. R2-3 regression surface:

- /v1/transactions             → mpc:transactions:create
- /v1/subscriptions/*          → mpc:subscriptions:write
- /v1/payment-requests/*       → mpc:payment-requests:write
- /v1/smart-wallets/*          → mpc:smart-wallets:write
- /v1/trade/*                  → mpc:trade:write
- /v1/intents/* (sign/co-sign) → mpc:sign (same perm as /v1/mpc/sign)
- /v1/mpc/operations/approve,reject → mpc:operations:approve

Bare role=="api" is no longer sufficient anywhere — an API key with empty
permissions hits 403. Human roles (owner/admin/signer) still pass through
on JWT auth. Also adds the canonical requireRoleOrAPIPermission helper to
middleware.go.
2025-12-28 10:42:56 -08:00
zeekay d484dbe82d fix(mpc): F2/F10/F20/F23 — CAS on approve + enum validation + pagination total
- F2: handleApproveOperation now runs tryApproveOperation inside
  RunInTransaction with CAS guard on (ApprovedBy, Status). Two
  concurrent final-approvers can no longer both satisfy quorum and
  both trigger signing; exactly one wins, the other retries or sees
  "operation not in pending_approval state". 5 retries with backoff,
  then 409 conflict.
- F23: initiator can NEVER self-approve, regardless of how many
  approvals already exist. Previous code only blocked self-approval
  when len(ApprovedBy) == 0.
- F10: list handlers (operations + audit) now call Count() on the
  filtered query before fetching the page; totalItems reflects the
  real count, not len(page). Added page/perPage response fields.
- F20: operation status filter is validated against the spec enum
  (pending_approval, approved, signed, rejected, failed, expired).
  Unknown values return 400 instead of silently dropping the filter.
2025-12-28 10:36:19 -08:00
zeekay 3a87134d56 fix(mpc): F3/F21/F24/F25 — CAS on session consume + reaper loop + policy gate
- F3: consumeSessionForSign now atomically CAS-guards OperationsUsed
  and ValueAccum inside RunInTransaction with explicit prev-value
  check; mismatch returns errSessionConflict and the outer loop
  retries up to 5 times. Prevents over-consuming sessions under
  concurrent /sign requests. Callers must present a sessionID (403
  otherwise — aligns with F1 handler change).
- F21: session reaper now loops pagination until a page produces no
  reaps; per-row Update errors are logged via an audit entry instead
  of being silently swallowed. Added reapExpiredSessions for direct
  test-drive.
- F24: handleCreateWalletSession now consults wallet policies. If the
  requested valueLimit trips a require_approval policy the session is
  created in pending_approval state; if a deny policy matches, the
  request is rejected 403. operationLimit > 1000 also forces approval.
- F25: enforced scope strictly — sign scope is required on the session
  for /sign and /settlement/sign. Sessions granted only 'authorize' or
  'read' cannot sign.

Added concurrency test cas_concurrency_test.go:
  TestConsumeSessionForSign_CASUnderRace (F3 reproducer)
  TestApproveOperation_CASUnderRace (F2 reproducer)
  TestInitiatorCannotApprove_Regardless (F23 regression)

Tests require -tags json1 for SQLite json_extract; Makefile already
sets this on the test target (F30 mitigation).
2025-12-28 10:29:41 -08:00
zeekay 8e644b8fe3 fix(mpc): F1/F4/F5/F6/F14/F15/F22 — harden /v1/mpc/sign + biometric + default wallet
- F1: /v1/mpc/sign now requires sessionId (403 without). Same for
  settlement/sign. No silent auto-session selection in handlers.
- F15: /v1/mpc/sign now gated by requireRole("owner","admin","signer","api")
  to match /v1/mpc/settlement/sign.
- F6: decodeMessage is strict — returns error when the declared encoding
  does not parse. Removed the raw-string fallback that made the endpoint
  an arbitrary-bytes signing oracle.
- F4: /v1/mpc/balances/{address} now resolves the address against the
  caller's org (ETH/BTC/SOL columns). Unknown or cross-tenant address
  returns 404 — no more "empty balances" leak.
- F5: biometric enroll requires a server-issued challenge, signed
  attestation (P-256 ECDSA verify), and a SecureGate liveness score
  above 0.8 (PAD-2). Raw templates are no longer accepted. Added
  consumeBiometricChallenge + verifyBiometricAttestation helpers.
- F14: added Wallet.DefaultForUserID column; handleMpcSetDefaultWallet
  now writes to it instead of stuffing a "default:<userID>" sentinel
  into the user-facing Name field. Previous default (if any) is cleared
  atomically per (orgId, userId).
- F22: findDefaultWalletID reads the new is_default column (via
  defaultForUserId) — no more "most recently created wallet" fallback
  that produced non-deterministic behavior.
2025-12-28 10:23:04 -08:00
zeekay da6efbd5bc feat(api): /v1/mpc/* spec surface (wallets, sessions, operations, policies, audit)
Implement the canonical Liquidity MPC surface frozen in
openapi/mpc.yaml. All endpoints mount under /v1/mpc/* — one and only
one path per operation, no aliases, no backwards compatibility.

New:
- handlers_mpc.go — wallets, balances, default wallet, sign,
  settlement/sign, webauthn/biometric spec shapes
- handlers_sessions.go — time-bounded signing grants with operation
  and value limits, enforced on every /sign via consumeSessionForSign
- handlers_operations.go — unified Operation view over db.Transaction
  with kind ∈ {sign,send,mint,burn,transfer,contract_call}

Changed:
- server.go: mount r.Route("/mpc", …); remove top-level /v1/policies
  and /v1/audit (moved to /v1/mpc/policies, /v1/mpc/audit); retain
  /v1/transactions POST as internal wiring
- handlers_policy.go: accept both {id} (internal) and {policyId} (spec)
- pkg/db/models.go: add Session model (registered as "mpc-session")
2025-12-28 10:09:49 -08:00
zeekay 507cbd6355 feat: REST + WebSocket + ZAP transports on every MPC node
- ws.go: EventBus pubsub + /v1/ws upgrade handler (stdlib, no deps)
- zap_server.go: StartZAP(port) with opcodes 0x0060-0x0064
  keygen, sign, status, wallets, intents over ZAP binary protocol
- Events field on Server for cross-transport event broadcasting
- Each consensus node serves full API (not separate mpc-api sidecar)
2025-12-28 07:11:02 -08:00
zeekay 847ddebc2e fix: remove postgres/valkey/legacy mode — consensus+ZapDB only
- Removed --mode, --api-db, --api-kv, --db flags
- Removed legacy NATS/Consul transport (runNode, ~200 lines)
- Removed standalone API server (runAPIOnly, ~200 lines)
- Removed Consul helpers, NATS connection, interactive prompts
- Dashboard now uses ZapDB (SQLite) instead of postgres
- pkg/db: removed postgres backend, removed valkey client
- Consensus mode is the only mode. Zero external deps.
2025-12-28 06:18:03 -08:00
zeekay dd35222450 Merge hanzo/mpc additions: ConsensusKV, HSM providers, policy engine, rename CLI to mpc-cli
- pkg/infra: Add abstract KV interface, ConsensusKV (dual-cert BFT with
  Ed25519 + ML-DSA-65), NatsKV, ConsulKVAdapter, HybridKV, SQLiteMeta
  with full test coverage
- pkg/hsm: Add Provider interface and implementations for AWS KMS,
  GCP Cloud KMS, Azure Key Vault, Zymbit HSM, Hanzo KMS, and file-based
  providers with registry and tests. Rename existing FileProvider to
  FilePasswordProvider to avoid collision with new Provider interface
- pkg/policy: Add transaction governance engine with signers, roles,
  spending limits, whitelist/blacklist, time windows, rate limiting,
  approval workflow, audit log, and FHE private policy evaluation
- contracts: Add ThresholdPolicy.sol for on-chain policy enforcement
- cmd: Rename lux-mpc-cli to mpc-cli
- Makefile: Update build targets for mpc-cli
2025-12-28 03:12:38 -08:00
zeekay 10dcad018e fix: address red team findings — auth, TLS, KMS fallback, AAD
M-1:  compose.yml commented section now enforces JWT_SECRET required syntax
M-3:  TLS client config uses TOFU certificate pinning instead of blind
      InsecureSkipVerify — SHA-256 fingerprint pinned on first connect,
      mismatches rejected on subsequent connections
M-4:  TLSOnlyListener plaintext escape hatch gated behind both
      ALLOW_PLAINTEXT=true AND ENVIRONMENT=development
M-5:  KMS fallback to unencrypted storage is now log.Fatal in production
      (ENVIRONMENT != "development"), including Put-time failures
M-6:  isKeyShare() now checks org: prefix, key-type prefixes (frost:,
      bls:, sr25519:, tfhe:), and hex wallet IDs instead of returning true
M-9:  AES-GCM Encrypt/Decrypt accept optional AAD variadic parameter
      for context-binding; existing callers remain backward compatible
M-12: Internal /keygen endpoint requires org_id in request body (400 if
      missing); orgID propagated to GenerateKeyMessage and audit log
2025-12-28 02:59:23 -08:00
zeekay 68959865c1 security: harden internal MPC API, enforce TLS-only transport, restrict egress
- Add bearer token auth to internal MPC API (port 9800): /keygen, /keys,
  /backup now require Authorization header. Token from MPC_INTERNAL_API_KEY
  env (KMS-synced) or derived from Ed25519 node identity.
- Replace DualModeListener with TLSOnlyListener: plaintext peer connections
  are rejected. TLS 1.3 with PQ key exchange is mandatory.
- Add dedicated 20 RPM rate limiter on bridge signing and intent signing
  endpoints (separate from global 100 RPM).
- Add body size limit (1MB), read/write timeouts, and 10 RPM rate limiter
  to internal MPC API server.
- Restrict K8s NetworkPolicy egress for MPC nodes to peers, postgres,
  valkey, DNS, and HTTPS only (was unrestricted).
- Wire cluster-api-key in api-deployment for authenticated proxy to
  internal MPC API. Forward as Authorization: Bearer header.
- Remove hardcoded dev JWT_SECRET default from compose.yml (now required).
- Add internal-api-key to KMSSecret CRD and StatefulSet env.
2025-12-28 02:52:46 -08:00
zeekay 6857bdfbda feat: Base framework + consensus default + minimal ops
- Mount chi handlers on Hanzo Base (lifecycle, health, auth framework)
- Default mode: consensus (ZAP p2p) — NATS/Consul deprecated
- compose.yml: 2 services (api + dashboard), SQLite, no external DB
- config.yaml: consensus mode, SQLite-first
- Makefile: simplified build targets, docker compose format
- K8s: kustomization.yaml for idempotent deploys
- Removed listenAddr from NewServer (Base handles listening)
- Legacy NATS/Consul preserved but not default
2025-12-28 02:32:54 -08:00
zeekay 55a6d85e10 feat: SQLite default — zero external deps to start
db.New() auto-detects backend from DSN:
- "" or "sqlite://path" or "file.db" → SQLite (default)
- "postgres://..." → PostgreSQL (production upgrade)

KV (Valkey/Redis) is optional — pass "" to skip.
No postgres, no redis required to run mpcd out of the box.
Removed pgxpool direct dependency from db package.
2025-12-28 02:26:17 -08:00
zeekay 6f4b0d52bb fix: remove all tenant-specific data from MPC infrastructure
MPC is generic infrastructure — no hardcoded tenant URLs.
- Remove pars.network, zoo.network from OIDC issuers, ingress, TLS
- Remove redacted.com, redacted from CORS, WebAuthn, tests
- CORS/WebAuthn origins now env-driven: MPC_CORS_ORIGINS, MPC_WEBAUTHN_ORIGINS
- Ingress: mpc.lux.network only — tenants route through their own ingress
- Tests updated to match infrastructure-only origin lists
2025-12-28 02:19:39 -08:00
zeekay 522b59b1a8 fix: remove /api/ prefix from all routes — use /v1/ only
Subdomain routing (mpc.lux.network/v1/) not path prefix (/api/v1/).
Updated: server routes, KMS routes, main.go forwarding, compose,
K8s dashboard config, ingressroute, landing page HTML.
2025-12-28 02:13:02 -08:00
zeekay 26fadacac5 fix: deduplicate KMS JSON helpers, use shared writeJSON/writeError
Remove duplicate writeKMSJSON/writeKMSError from handlers_kms.go.
Reuse writeJSON/writeError from middleware.go — one way to do things.
2025-12-28 01:59:47 -08:00
zeekay b235fbd4e6 test: 33 regression tests for all security fixes
- handlers_kms_test.go: input validation, orgID from context not header
- auth_test.go: JWT iss/aud enforcement, refresh/access token separation
- ratelimit_test.go: clientIP uses RemoteAddr, ignores XFF
- server_test.go: maxBodySize middleware enforcement
2025-12-28 01:53:10 -08:00
zeekay e78eb68d9c security: fix remaining Red team findings (LOW + MEDIUM)
MEDIUM: Bridge wallet org isolation — added orgID filter to wallet query
  in handleBridgeSign, prevents cross-tenant signing.
LOW-1: JWT iss/aud claims — tokens now carry issuer "mpc.lux.network"
  and distinct audiences ("mpc-api" vs "mpc-refresh"). Refresh tokens
  can no longer be used as access tokens.
LOW-2: Rate limiter XFF bypass — clientIP uses r.RemoteAddr (set by
  RealIP middleware) instead of parsing X-Forwarded-For directly.
LOW-3: Request body size limit — 1MB maxBodySize middleware prevents OOM
  from oversized payloads.
2025-12-28 01:46:33 -08:00
zeekay c1f772aac9 security: fix 3 critical findings from Red team review
CRITICAL-1: KMS routes registered outside auth middleware — moved inside
  authenticated route group with requireRole("owner", "admin")
CRITICAL-2: X-Org-ID header trusted without validation — replaced with
  getOrgID(r.Context()) from JWT claims
CRITICAL-3: (KMS standalone addressed in lux/kms repo)
2025-12-28 01:39:55 -08:00
zeekay 5ca7e95c5b feat: merge KMS validator key management into MPC binary
KMS routes now live inside MPC API server — one binary for both:
- POST /api/v1/keys/generate — create validator BLS + Ringtail keys
- GET /api/v1/keys — list all validator key sets
- GET /api/v1/keys/{id} — get validator key set
- POST /api/v1/keys/{id}/sign — sign with validator key
- POST /api/v1/keys/{id}/rotate — reshare validator keys

Storage: ORM (same postgres/ZapDB as MPC wallets).
No separate KMS binary needed — mpcd serves both.
2025-12-28 01:33:18 -08:00
zeekay ee69129a3c fix: remove duplicate bridge watchers, expose Handler for Base mount
- Remove pkg/bridge/ (btc_watcher + orchestrator) — bridge/opnet-watcher
  is the single source of truth for chain watching per CTO directive
- Add Handler() to API server for mounting on Hanzo Base
- MPC is signing-only; bridge is the control plane
2025-12-28 01:26:41 -08:00
zeekay 3764f7cd60 feat: configurable WebAuthn RP ID and origins for Liquidity exchange
Add MPC_WEBAUTHN_RP_ID and MPC_WEBAUTHN_ORIGINS env vars to make
WebAuthn relying party and origin allowlist configurable. Adds
Liquidity exchange origins (exchange.{dev,test,main}.redacted.com,
redacted) to WebAuthn origin checks, CORS, and security tests.
2025-12-28 01:13:26 -08:00
zeekay 4e915d77c3 security: fix 15 custody audit findings, add regression + fuzz tests
Fixes all findings from the Blue Team defensive security review of the
redacted → redacted custody architecture transition.

HIGH:
- S-1: Implement P-256 ECDH + AES-256-GCM encryption of passkey backup shards
- W-2: Reject unparseable WebAuthn public keys (was silently skipping verification)
- O-1: Add Ed25519 binary signature verification package (pkg/integrity)

MEDIUM:
- W-1: Validate attestation is a real P-256 curve point, not raw attestation object
- W-3: Add origin validation to all WebAuthn flows (register, verify, trade approve)
- N-2: Remove plaintext TCP fallback from MPC node transport (TLS mandatory)
- K-1: Fail-closed on empty/whitespace orgID (prevents cross-tenant key leakage)
- A-1: Make AuditEntry immutable (Update/Delete return error)
- A-2: Add MPC-level signing audit log (wallet, message hash, intent, trigger)
- O-3: Invalidate backup shards after key reshare (stale shares are useless)
- O-2: Reject env/file password providers in production (require cloud HSM)

LOW:
- T-1: Add error handling + MarkSignFailed to signing goroutine
- T-2: Verify caller authorization in handleTradeSubmit
- K-2: Secret erasure stub now calls runtime.GC() instead of no-op
- N-1: Add VerifyConnection callback to reject peers without TLS certs

Tests: 69 unit + 7 fuzz targets, ~900K fuzz executions, 19/19 packages green.
Makefile: add -tags json1 for SQLite JSON function support in ORM tests.
2025-12-28 01:00:12 -08:00
zeekay 21fc7ef1c2 feat: passkey-encrypted backup shard — company CANNOT decrypt
Backup shard (shard 3) is encrypted with user's WebAuthn passkey
public key (P-256 ECDH). HSM stores the ciphertext but cannot
decrypt — only user's biometric (via Secure Enclave private key) can.

ATS shard (2) + encrypted_backup (3) = company CANNOT sign.
User shard (1) + passkey_decrypt(3) = user CAN exit independently.

Added UserPasskeyFingerprint to WalletBackup model.
Backup handler looks up user's WebAuthn credential for encryption.
2025-12-28 00:53:34 -08:00
zeekay 49e96324c8 feat: add biometric trade approval flow with push notifications
- Trade approval service: submit, approve (WebAuthn), reject, pending
- Push notifier: APNS (iOS) + FCM (Android) for trade approval requests
- Biometric verification: Face ID/Touch ID with 30s freshness, nonce replay guard
- Custody policy engine: KYC/AML/sanctions/limits checks before co-signing
- Device store: register/revoke mobile devices with Secure Enclave keys
- API routes: POST /api/v1/trade/{submit,approve,reject}, GET pending
- Trade expiry reaper: cleanup expired approvals (>5 min)
- 17 custody tests + 33 API tests passing
2025-12-28 00:46:57 -08:00
zeekay 9c5308750d fix: rename env vars to MPC_PASSWORD, remove LUX_ prefix
- Default env var: MPC_PASSWORD (was LUX_MPC_PASSWORD)
- No fallback to ZAPDB_PASSWORD — single source
- Factory env_var config reads from config map only (not env)
- Cloud HSM providers unchanged (ZAPDB_ENCRYPTED_PASSWORD)
- All tests pass
2025-12-28 00:40:20 -08:00
zeekay ad4f429f1e fix: per-org MPC isolation — orgID end-to-end threading + security hardening
- M-1 CRITICAL: Remove legacy key share fallback (cross-tenant leak)
- M-2 HIGH: orgID now required string on all Node methods (not variadic)
- M-3 HIGH: Key material zeroing after signing sessions
- Thread orgID through MPCBackend interface, all API handlers
- Reshare + TFHE sessions use OrgScopedKey for reads/writes
- Remove hardcoded JWT default (fail-hard if not set)
- Add warning log for empty orgID legacy path
- Regression tests: TestGetKeyShareWithFallback_M1_CrossTenantLeak
2025-12-28 00:20:28 -08:00
zeekay b2bb158931 feat: per-user shard encryption + wallet isolation
EncryptedKVStore: AES-256-GCM wrapper over ZapDB for per-user shard encryption.
Each user's key shards encrypted with a unique DEK:

  user DEK = HMAC-SHA256(masterKey, "mpc:shard:" + orgID + ":" + userID)
  org DEK  = HMAC-SHA256(masterKey, "mpc:org:" + orgID)

Three layers of MPC data protection:
  1. ZapDB ChaCha20-Poly1305 (node-level, existing)
  2. EncryptedKVStore AES-256-GCM (per-user, NEW)
  3. API filter on createdBy (access control, NEW)

Even if ZapDB is fully dumped, individual users' shards are
independently encrypted. Wrong user key → decryption fails.

5 new tests: roundtrip, key uniqueness, wrong-key rejection,
empty values, org vs user key separation.
2025-12-28 00:13:50 -08:00
zeekay dc2e060db9 feat: per-user wallet isolation — non-admin users only see their own wallets 2025-12-28 00:07:13 -08:00
zeekay 8902945c46 security: fix keygen org-scoping + orgID injection prevention
Three fixes:

1. OrgScopedKey sanitizes colons in orgID to prevent key injection
   via the ":" separator (e.g. orgID "evil:org:fake" would create
   ambiguous key paths).

2. All 5 keygen sessions (CGGMP21, FROST, BLS, SR25519, LSS) now
   write key shares with OrgScopedKey(orgID, key) instead of bare
   keys. Previously keygen wrote unscoped but signing read scoped,
   causing key-not-found errors in multi-tenant deployments.

3. Added orgID field to session struct and threaded it through all
   keygen constructors and Node.Create*KeyGenSession methods using
   variadic orgID ...string for backward compatibility.
2025-12-27 23:47:21 -08:00
zeekay 41c7566348 feat: enable Go 1.26 runtime/secret for forward secrecy of key shares
Add GOEXPERIMENT=runtimesecret to Makefile, CI, and Dockerfile builds.
Wrap key share loading in all 5 signing session types with
withSecretErasure() so stack/register state containing raw share bytes
is zeroed after parsing. Build-tagged stub ensures everything compiles
without the experiment flag.
2025-12-27 23:20:52 -08:00
zeekay e40f0b6dff fix: goimports formatting with local-prefixes (matches golangci-lint) 2025-12-27 23:14:14 -08:00
zeekay 3954d11f50 fix: goimports formatting + CGO_ENABLED=0 for CI tests
- Run goimports -w on all 17 Go files with formatting issues
- Set CGO_ENABLED=0 on all CI steps (test, lint, build, CodeQL, SBOM)
- Remove -race flag from tests (requires CGO which pulls in blst.h)
- All 17 test packages pass with CGO_ENABLED=0
2025-12-27 23:01:00 -08:00
zeekay 0d7a523c3e security: add orgID multi-tenancy isolation, rename badger to zapdb
Multi-tenancy isolation (addresses Dragonfire review MPC audit):
- Add OrgID field to all event messages (keygen, sign, reshare)
- Scope session dedup keys as orgID-walletID-txID
- Namespace KVStore keys with org prefix (org:<orgID>:<key>)
- Backward-compatible fallback to legacy unscoped keys
- New org_kvstore.go with OrgScopedKey/GetKeyShareWithFallback

Naming cleanup:
- Rename all badger references to zapdb in code, config, tests
- config.yaml: badger_password → zapdb_password
- AppConfig: BadgerPassword → ZapDBPassword
- Fix MPCiumVersion typo → MPCVersion
2025-12-27 22:41:08 -08:00
zeekay 4641ad7c09 feat: BLS threshold + security hardening + ZapDB KMS-backing
Security (from red team review):
- Replay protection: request_id + timestamp freshness on sign API
- Ed25519 message auth re-enabled on all protocol messages
- DKG timeout (5min) + signing timeout (30s) with watchdog
- Dedup map TTL cleanup (10min expiry, 2min sweep)

BLS threshold signing:
- KeyTypeBLS + keygen/signing sessions
- TrustedDealer pattern for BLS DKG
- CBOR serialization for BLS config
- 8 tests

ZapDB KMS-backing:
- HSM PasswordProvider wired to ZapDB init
- Supports: AWS KMS, GCP KMS, Azure KV, env, file
- Default env for backward compat
- 19 tests

Also: sr25519 sessions, fixed SignWireMessage build break
2025-12-27 21:54:46 -08:00
zeekay b143c542e4 feat: sr25519 threshold signing for Polkadot/Substrate
MPC daemon now supports sr25519 (Schnorrkel) key generation and
threshold signing using FROST over the ristretto255 group.

New files:
- pkg/mpc/sr25519_keygen_session.go — threshold keygen
- pkg/mpc/sr25519_signing_session.go — threshold signing
- pkg/mpc/sr25519_config_marshal.go — CBOR serialization
- pkg/mpc/ristretto255.go — local curve implementation
- pkg/eventconsumer/signing_handler_sr25519.go — event routing

Wiring:
- KeyTypeSR25519 constant + session type
- Event consumer routes sr25519 keygen/signing events
- API accepts "sr25519" as valid protocol
- DOT/KSM network codes mapped to sr25519 key type
- Signing context configurable (default "substrate")

14 tests passing.
2025-12-27 21:41:32 -08:00
zeekay 353850b5a7 feat: dashboard pages + security hardening for intents, settlements, tx lifecycle
Backend:
- Reorg detection in tx tracker (re-verify block hash, detect receipt disappearance)
- Post-finalization safety margin (6 extra blocks before stopping tracking)
- Server-side HSM co-signing (HSMProvider interface, server calls HSM directly)
- Background intent expiry reaper (5-min sweep for stuck intents)
- Domain-separated intent hashing (lux-mpc-intent:v1 prefix)
- Backup rate limiting (max 3 active per wallet)
- Default backup changed to 2-of-3 (iCloud + HSM + offline) for fault tolerance
- All status transitions now use RecordTransition with proper actors

Frontend:
- Intents list page with status filter
- Intent detail page with lifecycle progress bar, sign/co-sign actions
- Settlements list page with status filter
- Settlement detail page with lifecycle, HSM signatures, transfer agency
- Enhanced transaction detail with on-chain confirmation, revert reason, status history
- Wallet backup UI with Shamir shard visualization
- Updated StatusBadge with all new status colors
- Nav updated with Intents + Settlements links
- Co-sign UI updated for server-side HSM flow (key_id only, no client signature)
2025-12-27 18:16:15 -08:00
zeekay a42050c830 feat: tx lifecycle tracking, Shamir backup sharding, settlement engine
Transaction Lifecycle ("did the tx land?"):
- TxTracker polls eth_getTransactionReceipt for block inclusion
- Receipt fields: blockNumber, blockHash, receiptStatus, gasUsed, revertReason
- Confirmation counter with configurable target (default 12)
- StatusHistory records every state transition with timestamp and actor
- Stuck/dropped tx detection with webhook alerts

Backup Key Sharding (Secure Recovery Flow):
- GF(2^8) Shamir Secret Sharing (ShamirSplit/ShamirCombine)
- WalletBackup: AES-256-GCM encrypt key share, Shamir-split the key
- Labeled shard destinations: icloud, hsm, offline, custody, device
- Default 2-of-2 (iCloud + Platform HSM), institutional 3-of-5

Settlement & Intent Engine:
- Intent lifecycle: pending_sign → signed → co_signed → recorded → matched
- HSM co-signing as second signer in multisig flow
- Settlement with HSM multisig attestation collection
- TransferAgency hash registration and verification
- WalletBackup, Intent, Settlement DB models

API Routes:
- POST/GET /intents, /intents/:id/sign, /intents/:id/co-sign
- GET /settlements, /settlements/:id
- POST/GET /wallets/:id/backup
- New webhooks: tx.included, tx.finalized, tx.reverted, intent.*

40 tests passing across backup, settlement, txtracker, api packages.
2025-12-27 18:09:37 -08:00
zeekay e9df48d2eb feat(api): add landing page to dashboard API
Add HTML landing page at / route for mpc.lux.network showing:
- CGGMP21 and FROST protocol info
- Supported chains (BTC, ETH, LUX, SOL, XRPL, TON, etc.)
- Links to health, bridge dashboard, and API docs

Also update go.work to Go 1.26.1.
2025-12-27 14:24:28 -08:00
zeekay 0e3349fdc4 feat: bridge admin dashboard, WebAuthn device management, LSS protocol selection
- Bridge admin pages: /bridge (overview), /bridge/wallets (signing wallet
  selector), /bridge/fees (fee config), /bridge/contracts (BRIDGE_ROLE mgmt)
- Bridge config API: GET/PATCH /api/v1/bridge/config, GET /bridge/networks
- BridgeConfig model in DB (org-scoped, stores wallet ID, fees, toggles)
- WebAuthn device management: /settings/devices (register/delete devices)
- Biometric approve button on transaction detail page using WebAuthn API
- Dashboard lib/webauthn.ts with startRegistration, startAuthentication
- Protocol selection in wallet creation wizard (CGGMP21, LSS, FROST)
- Protocol field on Wallet model, validation in create handler
- Wallet detail page shows protocol type
- Bridge nav link in sidebar
2025-12-27 12:12:02 -08:00
zeekay d377958319 fix: dual-mode TLS listener for zero-downtime PQ TLS rolling upgrades
- DualModeListener sniffs first byte: 0x16 → TLS, else plaintext
- Client falls back to plaintext TCP if TLS dial fails
- Enables rolling upgrades without breaking peer connectivity
- All nodes now running PQ TLS 1.3 (X25519MLKEM768) in production
2025-12-27 12:05:25 -08:00