Commit Graph
15 Commits
Author SHA1 Message Date
zeekay 37e93fb4de 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 fdacb58389 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
zeekay e2fc7dab6c 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.
2026-04-18 18:05:11 -07:00
zeekay 9d30a382b6 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).
2026-04-18 17:30:14 -07:00
zeekay 8c569f04cd 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/
2026-04-18 17:02:33 -07:00
zeekay af9c6b1d54 docs: /v1/mpc/* surface + session enforcement in LLM.md 2026-04-18 15:18:39 -07:00
zeekay 1dc0a3f633 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.
2026-04-07 14:00:47 -07:00
zeekay c7f126adbf 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
2026-03-19 01:15:33 -07:00
zeekay 8b66b6ffd9 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.
2026-03-18 14:09:08 -07:00
zeekay f711f4770f feat: API key permissions in context, clean up stale files
- validateAPIKey returns *db.APIKey so middleware has access to permissions
- API key permissions propagated into request context via ctxPermissions
- requirePermission middleware checks actual key permissions (not just role)
- PasswordHash/MFASecret fields marked api:"-" to prevent serialization
- Remove stale generated files (CHANGELOG, CI_STATUS, DOCKER_BUILD etc.)
- Update LLM.md with production deployment state and architecture decisions
2026-03-01 17:03:06 -08:00
zeekay f8f75c732b chore: sync dependencies and format code 2026-02-04 15:47:51 -08:00
zeekay efa0853421 Sync improvements from hanzoai/mpc
Features:
- Add FROST keygen and signing sessions for EdDSA/Taproot
- Add LSS keygen and signing sessions with dynamic resharing
- Add CBOR serialization for FROST and LSS configs (fix JSON corruption)
- Add recovery byte calculation for Ethereum compatibility
- Add FROST signing handler for EdDSA signatures
- Upgrade threshold library to v1.5.2

Security:
- Fix config serialization using CBOR instead of JSON
- Add security audit findings to LLM.md
- Document blockchain support matrix

Files added:
- frost_config_marshal.go, frost_keygen_session.go
- lss_config_marshal.go, keygen_session_lss.go
- signing_session_frost.go, signing_session_lss.go
- recovery.go, signing_handler_frost.go
2026-01-25 03:37:47 -08:00
zeekay 6c52623dc0 Add docs 2025-12-13 00:12:21 -08:00
zeekay 0b8661ced9 Use new threshold lib 2025-08-02 15:49:26 -05:00
zeekay 4ec50522c9 Add benchmarks, tests, updated docs 2025-08-02 15:27:50 -05:00