Commit Graph
709 Commits
Author SHA1 Message Date
zeekay 2e94deef62 Merge kms-nonce-bind-2026-04-28
# Conflicts:
#	CHANGELOG.md
2026-04-28 00:58:33 -07:00
zeekay 41c32bd4a8 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 211047dcc3 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.
v1.10.2
2026-04-28 00:01:45 -07:00
zeekay 4e0387bb91 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 d627c84 binding fix.
2026-04-27 21:36:14 -07:00
zeekay 70e0899973 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 77f99c1f48 k8s: probe /healthz (v1.10.0 renamed /health → /healthz)
The internal API now exposes /healthz returning JSON readiness state.
/health returns 404.
v1.10.1
2026-04-25 21:50:50 -07:00
zeekay ff25525a48 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 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 2dfb53454f 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 65c056f803 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 0e3c728275 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)
v1.10.0
2026-04-25 21:21:02 -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
098634ffe8 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 867663b38a 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.
v1.9.2
2026-04-22 03:53:56 -07:00
zeekay a6aef24e5a fix(mpc): HasSigningQuorum uses operator-facing threshold semantics
Follow-up to 0c38bbd. 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).
v1.9.1
2026-04-20 18:52:49 -07:00
zeekay 0c38bbdea6 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 3e0d685b44 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
zeekay 87c9867610 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
a9505db 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/
v1.9.0
2026-04-18 22:41:45 -07:00
zeekay 74ac84e908 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
v1.8.0
2026-04-18 21:07:26 -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.
v1.7.1
2026-04-18 18:05:11 -07:00
zeekay 3b3ec81d04 ci: run Postgres-backed CAS/audit tests via service sidecar
pkg/api/cas_postgres_test.go and the audit-chain concurrency test in
hanzo/kms silently SKIP when no Postgres is reachable. Without a live
sidecar, regressions in the FOR UPDATE + advisory-lock paths would
sail through PR checks — exactly the R3-1 / R2-2 class of bugs the
tests exist to catch.

Add a postgres:15-alpine service, expose TEST_PG_DSN to the test job,
and psql-ping the sidecar before `go test` so the run fails fast if
the service is misconfigured instead of skipping.

One gate, always on, no special-case runbook.
v1.7.0
2026-04-18 17:33:26 -07:00
zeekay d8640871cf 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).
2026-04-18 17:32:56 -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 55cf849922 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.
2026-04-18 17:13:07 -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/
v1.6.0
2026-04-18 17:02:33 -07:00
zeekay c3c9ca2900 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.
2026-04-18 16:46:41 -07:00
zeekay 8033288726 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.
v1.5.4
2026-04-18 16:07:57 -07:00
zeekay 05ee270c0a 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).
2026-04-18 16:07:48 -07:00
zeekay 18abb65851 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.
2026-04-18 16:07:29 -07:00
zeekay af9c6b1d54 docs: /v1/mpc/* surface + session enforcement in LLM.md 2026-04-18 15:18:39 -07:00
zeekay 06954b4a7f 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")
2026-04-18 15:18:36 -07:00
zeekay cb80ef4dd6 chore: exclude missing luxfi/genesis v1.5.21/22, sync edwards25519 replace 2026-04-18 15:18:16 -07:00
zeekay a8961d25c0 fix: lattice v7.0.2 — fresh tag, no stale proxy cache v1.5.3 2026-04-18 09:10:44 -07:00
zeekay 0ba3a2fcb1 fix: remove GOPRIVATE — luxfi/hanzoai repos are public
No GITHUB_TOKEN needed. No git config override. No GOPRIVATE.
Proxy downloads everything. GONOSUMDB skips sum DB for our repos.
go.sum checksums match proxy (not direct).
v1.5.2
2026-04-18 09:08:06 -07:00
zeekay ab7ff02e10 fix: edwards25519 v0.1.0 (published go.mod), lattice direct-download hash
- luxfi/edwards25519 now has go.mod (v0.1.0 tag)
- Replace agl/ed25519 → luxfi/edwards25519 v0.1.0
- go.sum lattice hash matches GOPRIVATE direct download (not proxy)
v1.5.1
2026-04-18 09:06:00 -07:00
zeekay 724c7487a4 fix: update go.sum with proxy-matching lattice checksum, clean Dockerfile v1.5.0 2026-04-17 21:40:56 -07:00
zeekay feb4572356 fix: vendor in Docker — resolves replace directives + checksum issues v1.4.9 2026-04-17 21:39:15 -07:00
zeekay babac24933 fix: scoped -mod=mod on build only, precise lattice h1: strip v1.4.8 2026-04-17 21:36:30 -07:00
zeekay 6a79d5cfaa fix: strip lattice checksum AFTER COPY . . (overrides sed) v1.4.7 2026-04-17 21:34:11 -07:00
zeekay d18fdf29c2 fix: strip stale lattice checksum, keep go.sum for replace directives v1.4.6 2026-04-17 21:32:21 -07:00
zeekay b8af86ab78 fix: dockerignore go.sum, fresh download without stale checksums v1.4.5 2026-04-17 21:30:01 -07:00
zeekay d48cdfb15c fix: -mod=mod auto-updates go.sum, scoped GONOSUMCHECK for private repos only v1.4.4 2026-04-17 21:27:58 -07:00
zeekay 0540d02b6f fix: GOPROXY=direct bypasses stale module proxy cache v1.4.3 2026-04-17 21:20:11 -07:00
zeekay cca3bf5e0a fix: go mod tidy regenerates go.sum with fresh checksums v1.4.2 2026-04-17 21:17:51 -07:00
zeekay a84b97599a fix: rm go.sum before build, GONOSUMCHECK on all go commands v1.4.1 2026-04-17 21:15:45 -07:00
zeekay ef432372bb fix: download deps without go.sum to skip checksum verification v1.4.0 2026-04-17 21:13:40 -07:00
zeekay 0c4578917d fix: GOFLAGS=-mod=mod in Dockerfile to handle checksum mismatches v1.3.9 2026-04-17 21:12:10 -07:00
zeekay 17d0d74159 fix: GONOSUMCHECK in Dockerfile (hanzoai/base checksum mismatch) v1.3.8 2026-04-17 21:10:01 -07:00
zeekay 18f6ed43ad ci: use UNIVERSE_PAT for private dep access in docker build v1.3.7 2026-04-17 21:08:20 -07:00
zeekay 59421450b2 ci: fix notify job dependency (needs: manifest not docker) v1.3.6 2026-04-17 21:03:34 -07:00