Skip to content

feat(samples): MPC custody API signing challenge example (challenge + verify + ES256 assertions + withdrawal) - #77

Open
enforser wants to merge 1 commit into
mainfrom
custody-api-signing-example
Open

enforser wants to merge 1 commit into
mainfrom
custody-api-signing-example

Conversation

@enforser

@enforser enforser commented Nov 10, 2026 •

Copy link
Copy Markdown

What

A single-file Python example (samples/python/custody_withdrawal.py) for the MPC custody API signing challenge and authorized withdrawals.

Prerequisite (stated prominently in the README and the example docstring): this API is not self-serve. Integrators must contact Gemini support, who enable the custody API for the account group, configure the MPC custody account's approved security policy with a positive API signer threshold, and register the customer's API signer keypairs (COSE_Key-encoded P-256 public keys) on the account. Every withdrawal fails closed until that enrollment is complete.

Why the verification step matters

The hard part of integrating this API is client-side: a verifier must reconstruct the canonical authorization context byte-for-byte and re-derive the challenge. Any re-serialization (different key order, whitespace, re-encoding of the embedded signing strings) produces a different digest. The example therefore embeds the verification engine in the same file and runs it on every flow:

  • rebuilds the canonical context from the complete received action/policy signing strings (never re-serialized, never trusting a server-supplied digest)
  • SHA-256 → unpadded base64url → GEMINI-API-SIGNATURE-V1:<challenge>
  • validates the response against a locally trusted intent: account, single-leg transfer shape, asset/amount/address/memo, policy quorum, expiry window
  • raises field-specific VerificationErrors for every mismatch

The verification section is deliberately stdlib-only and keyless, so integrators can audit it or lift it into their own language.

Files

File Purpose
samples/python/custody_withdrawal.py End-to-end flow + embedded verifier: signed POST /v1/custody/challenge → local challenge verification → ES256 (COSE P-256) assertion per approved signer → signed POST /v2/withdraw/:network/:ticker
samples/README.md New "MPC Custody Withdrawals (Support-Enrolled Accounts Only)" section with the prerequisite, flow, and usage; endpoint table/structure updates
samples/python/requirements.txt Adds cryptography (ES256 signing in the example only; the verification section stays stdlib-only)

Contract

Implements the /v1/custody/challenge wire contract, version "1" (message prefix GEMINI-API-SIGNATURE-V1:). The protocol constants are carried as code so drift against a future contract version fails loudly.

Validation

  • End-to-end against production: XRP withdrawal with a 3-of-3 signer threshold — challenge minted, challenge verified by the embedded verifier against the live response, three ES256 assertions built, withdrawal accepted (HTTP 200)
  • The assertion builder was exercised against a real registered P-256 key: the COSE encoding matches the registered public key byte-for-byte
  • Note: on the wire, a transfer leg omits memo (rather than sending null) when a transfer has no memo; the verifier treats absent and null identically

@sdlc-pr-bot

sdlc-pr-bot Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

Change Control Evidence Check

Authorization — ❌ Fail

Evidence:

  • Linear ticket: ⚠️ No ticket linked
  • Change owner: Matthew Fors
  • Type of change: Normal
Testing — ✅ Pass

Evidence:

  • Tests from CI checks: ➖ No tests ran
  • Security scans: ✅ Passed
  • Testing summary: ✅ Found in PR description
    • Summary: XRP withdrawal functionality was tested end-to-end against production with a 3-of-3 signer threshold, resulting in successful challenge verification, three ES256 assertions, and HTTP 200 acceptance. The assertion builder was validated against a real registered P-256 key with byte-for-byte COSE encoding match to the registered public key.
  • Evidence link: View run
Approval — ✅ Pass

Evidence:

  • Approver: Victor Reventos Rosario
  • Approval source: GitHub PR Review
  • Approval timestamp: 2026-11-11 02:03 UTC
Segregation of Duties — ✅ Pass

Validated:

  • PR author: Matthew Fors
  • Commit author(s): enforser
  • Approver: Victor Reventos Rosario
  • Result: Implementer and approver are different people ✅

Last checked: 2026-11-11 02:03 UTC

↩ Re-run Clarissa if tickets, CI, or approvals have changed.

data=b"",
)
try:
with urllib.request.urlopen(request, timeout=60) as response:
Single-file end-to-end Python example for MPC custody withdrawals:
POST /v1/custody/challenge -> independent local challenge verification
(rebuilds the canonical signing context and re-derives the SHA-256 challenge;
never trusts a server-supplied digest) -> ES256 (COSE P-256) assertions from
approved signers -> POST /v2/withdraw.

The verification engine is embedded in the same file (stdlib-only section).
README documents the support-enrollment prerequisite: custody API enablement,
account security policy with an API signer threshold, and signer keypair
registration must all be arranged with Gemini support before the API is
usable. Validated end-to-end against production (XRP, 3-of-3 threshold).
@enforser
enforser force-pushed the custody-api-signing-example branch from 2020765 to 1d0ad7c Compare November 10, 2026 23:37
@enforser
enforser marked this pull request as ready for review November 10, 2026 23:38
@switchboard-pr-review-assigner
switchboard-pr-review-assigner Bot requested review from deeputom and removed request for mars-steven November 10, 2026 23:40
@nostradamus-bot

Copy link
Copy Markdown

Nostradamus Risk Rating — Medium

samples/python/custody_withdrawal.py implements HMAC-SHA384 API signing and hand-rolled ES256/COSE P-256 assertions for MPC custody withdrawals — a privileged financial operation — in example code that integrators are likely to adopt verbatim; correctness of the custom COSE CBOR encoder and challenge-verification engine is the primary security concern. The change is confined entirely to samples/ with no production server-side attack surface introduced, placing it at Medium (Impact: High × Likelihood: Medium per NIST SP 800-30r1).

This branch has not been deployed

No deployments
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.

5 participants