Skip to content

feat(custody): add threshold signing and safe key rotation control plane - #158

Merged
Obiajulu-gif merged 1 commit into
Chainmove:mainfrom
harystyleseze:main
Jul 28, 2026
Merged

feat(custody): add threshold signing and safe key rotation control plane#158
Obiajulu-gif merged 1 commit into
Chainmove:mainfrom
harystyleseze:main

Conversation

@harystyleseze

Copy link
Copy Markdown
Contributor

Summary

Adds an off-chain custody control plane for Stellar issuer/distribution/treasury
accounts: signer roles and weighted thresholds per operation category, a
pluggable signer adapter so the app never touches raw secrets, replay-proof
envelopes, and overlap-safe signer rotation with a recovery path.

This repo had no existing transaction-signing/custody code (confirmed by a full
audit before starting) — only public-key config and a design doc
(docs/stellar-asset-and-soroban-design.md) specifying the target model. This PR
implements that model as a new lib/custody/ module, following existing
conventions in the codebase (maker-checker + versioned state from
lib/stellar/pool-assets.ts, the local-signer-now/KMS-later pattern from
lib/security/audit-checkpoint.ts, and the tamper-evident audit log).

What's done / resolved

  • Signer roles & thresholds: lib/custody/policy.ts defines default
    thresholds per category (issuance, payout, emergency, recovery, rotation),
    validated against Stellar-style weighted-multisig invariants
    (validateSignerSetInvariants rejects any threshold that would permanently
    lock an account).
  • HSM/KMS/external-signer adapter: lib/custody/signer-adapter.ts defines a
    SignerAdapter interface (getPublicKey/sign) that never exposes key
    material to the app. LocalDevSignerAdapter is a testnet-only reference
    implementation (ephemeral in-memory keys, never persisted) that is
    structurally incapable of running against mainnet. createExternalSignerAdapter()
    is the production contract stub — no vendor is mandated, per the issue's
    non-goal.
  • Network/source/sequence/time/memo/intent-bound envelopes:
    lib/custody/envelope.ts binds all of these into one hash; replay is
    rejected across networks, stale sequences, and expired approval windows.
    Cross-intent replay is rejected at submission time by recomputing the actual
    on-chain operations and comparing their hash to what signers approved
    (lib/custody/operations.ts).
  • Allowlists, limits, separation of duties, environment binding: payouts
    require a payoutPolicy (destination allowlist + per-operation/daily limits)
    on the active signer set, enforced before any request is created. Every
    approval is checked against real, currently-authorized signers — no
    fabricated identities can satisfy quorum (this was caught and fixed during
    review, see below).
  • Overlap-safe rotation, recovery quorum, rollback: lib/custody/rotation.ts
    — propose → approve → activate (old set enters a "retiring" overlap window,
    both sets valid) → retire (refuses while any request is still pending against
    the old set) → rollback (reactivates the previous set safely, never leaving
    zero active sets). A recovery quorum path handles lost/compromised signers.
  • Tamper-evident records: every approval request, approval, rotation, and
    ledger submission result is written through the existing
    logAuditEvent(..., criticalAction: true) into the append-only hash-chained
    audit log.
  • Incident documentation: docs/custody-signer-rotation.md covers signer
    compromise, lost signer, stuck sequence, partial signature/signer outage, and
    outage scenarios, plus known limitations.

Required tests (__tests__/lib/custody/, 81 tests, all passing)

Threshold matrices and invariant rejection, stale-sequence rejection,
cross-network/cross-intent replay rejection, rotation with pending approvals
(retirement blocked), signer outage (quorum tolerates non-participating
signers), recovery-quorum rotation, and secret-scanning (no raw key material
ever returned by the signer adapter or logged).

Process note

This went through two rounds of independent code review. The first found a
critical gap (approveRotation didn't verify approvers were real signers,
letting fabricated identities satisfy quorum) plus 4 lower-severity issues
(a rollback ordering bug that could zero out active signer sets, the payout
allowlist/limits being implemented but never wired in, quorum counting heads
instead of signer weight, and the default submit path never producing an
"ambiguous" classification for network timeouts). All 5 were fixed and
re-reviewed to a clean pass; 3 minor non-blocking edge cases are documented
under "Known limitations" in the runbook doc.

Non-goals honored

No custody vendor (KMS/HSM provider) is mandated — the adapter interface is
vendor-agnostic by design.

Closes #131

Adds an off-chain custody control plane for issuer/distribution/treasury
Stellar accounts: signer roles and weighted thresholds per operation
category (issuance, payout, emergency, recovery, rotation), a SignerAdapter
interface so the app process never receives raw signing secrets (testnet-only
LocalDevSignerAdapter for dev/CI, a documented KMS/HSM adapter contract for
production), network/source/sequence/time/memo/intent-bound envelopes with
cross-network, stale-sequence, and cross-intent replay protection, overlap-safe
signer-set rotation with a recovery quorum path and rollback, and tamper-evident
audit logging of every approval request, approval, and ledger result via the
existing audit-log infrastructure.
@Obiajulu-gif
Obiajulu-gif merged commit 834b21d into Chainmove:main Jul 28, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[HARD][CUSTODY] Add threshold signing and safe key rotation for issuer and distribution accounts

2 participants