Files
nick-doc/04 - Flows/Trezor Safekeeping Flow.md
Siavash Sameni dceaf82934 audit: 2026-05-30 full-codebase audit — report, issues, docs, runbooks
Full-codebase-audit 2026-05-30 outputs:
- Audit report: 09 - Audits/Full Codebase Audit - 2026-05-30.md
- 81 issue files ISSUE-055..135 (decisions + 1 skipped no-brainer).
- Scanner docs from scratch (was zero): architecture, data model, API ref, payment
  flow, operations runbook + repo README.
- Doc-sync updates across API reference, data models, flows, design system.
- Secret Rotation Runbook (08 - Operations) for the exposed credentials.
- Reusable workflow guide (07 - Development) + .claude/workflows/full-codebase-audit.js.

Issues remain status:open intentionally — the code fixes are uncommitted-then-committed
working-tree changes per repo and aren't "resolved" until merged/deployed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 18:48:04 +04:00

7.8 KiB

Last updated: 2026-05-29 — aligned with code (see Doc vs Code Audit Report)

Trezor Safekeeping Flow

This flow adds hardware-backed custody controls without replacing the current payment model. The backend never stores private keys. Trezor support starts as a single hardware signer and is designed to upgrade to multisig later.

Default mode: optional. Existing release/refund flows do not require Trezor proof unless TREZOR_SAFEKEEPING_REQUIRED=true.

Note (corrected 2026-05-29): The frontend Trezor implementation does exist in current code — the 2026-05-29 audit's "zero frontend implementation" claim was based on an older snapshot. The active surface is:

  • src/app/dashboard/admin/trezor/page.tsxTrezorSettingsView (registration + re-register UI)
  • src/web3/trezor/trezorConnector.ts → lazy-imports @trezor/connect-web (trezorGetXpub, trezorGetAddress, trezorSignMessage)
  • src/components/trezor-sign-dialog/TrezorSignDialog.tsx → build-instruction → sign-on-Trezor → enter-txHash → confirm
  • src/actions/trezor.ts → full API client (getTrezorAccount, getTrezorRegistrationMessage, registerTrezor, getTrezorOperationMessage, confirmRelease/confirmRefund) that builds the trezor: { message, signature } object

The legacy confirmReleaseTx/confirmRefundTx helpers in src/actions/payment.ts post only { txHash } (no trezor field), but they have no UI callers — the active admin release/refund path goes through TrezorSignDialogactions/trezor.ts, which satisfies the assertTrezorSignatureForOperation guard when TREZOR_SAFEKEEPING_REQUIRED=true.

Goals

  • Generate a fresh receive address per user/payment from a registered Trezor xpub.
  • Require a Trezor-produced signature before release/refund confirmation when safekeeping enforcement is enabled.
  • Keep the Request Network payment adapter and legacy provider abstractions intact while adding custody controls.
  • Preserve the existing Payment model and orchestration surface.

Actors

  • Admin — the only party who can request operation messages and submit verify-operation calls. The registered Trezor must belong to an admin account; the safekeeping guard validates against the admin's TrezorAccount.registrationAddress.
  • Any authenticated user — may call POST /api/trezor/register (no role restriction on that endpoint).

Registration

  1. The Trezor owner (typically an admin) connects a Trezor and exports an Ethereum account xpub, for example m/44'/60'/0'.
  2. Backend builds a registration challenge:
    • GET /api/trezor/registration-message?xpub=...&registrationAddress=...
  3. The registration address must be the first derived address from the xpub:
    • m/44'/60'/0'/0/0
  4. The owner signs the challenge with that Trezor address.
  5. Frontend submits:
    • POST /api/trezor/register
    • xpub
    • registrationAddress
    • proofMessage
    • proofSignature
    • optional basePath, deviceLabel
  6. Backend verifies:
    • xpub is public, not private.
    • registration address matches xpub-derived index 0.
    • signature recovers the registration address.
  7. Backend stores / updates the TrezorAccount record. Upsert behaviour: if a record already exists for the user, xpub, basePath, and label are updated, but nextAddressIndex and the existing addresses array are preserved via $setOnInsert. Old address records continue to reference the previous xpub — a xpub mismatch is therefore possible after re-registration.

Address Generation

To issue the next payment address:

POST /api/trezor/addresses/next
{
  "purpose": "deposit",
  "paymentId": "..."
}

Valid values for purpose (as enumerated in the schema):

Value Description
deposit Incoming payment address
release Address used in a release operation
refund Address used in a refund operation
other General-purpose address

The backend derives non-hardened receive addresses from the registered xpub:

m/44'/60'/0'/0/{index}

If a paymentId already has an address, the endpoint returns the same address instead of incrementing the index.

Transaction Approval (Admin-only)

POST /api/trezor/operation-message and POST /api/trezor/verify-operation are admin-only endpoints. Before a release/refund confirmation, the admin asks the backend for the exact operation message:

POST /api/trezor/operation-message
{
  "operation": "release",
  "paymentId": "...",
  "transactionHash": "0x...",
  "amount": 100,
  "currency": "USDT",
  "provider": "request.network"
}

The Trezor signs that message and the admin submits it. The frontend implements this flow via TrezorSignDialog, which calls getTrezorOperationMessage(), prompts the Trezor to sign, and then submits the release/refund confirmation through confirmRelease() / confirmRefund() in src/actions/trezor.ts with the full payload:

{
  "txHash": "0x...",
  "amount": 100,
  "trezor": { "message": "<canonical operation message>", "signature": "0x..." }
}

The trezor object is included whenever a signature was produced, satisfying the backend assertTrezorSignatureForOperation guard. (The older confirmReleaseTx/confirmRefundTx helpers in src/actions/payment.ts post only { txHash }, but they are unused legacy code with no UI callers.)

Enforcement Flag

TREZOR_SAFEKEEPING_REQUIRED=false

Default is permissive so existing Request Network release/refund flows continue to work. Set it to true only after registering the operating admin's Trezor account (the frontend signing flow via TrezorSignDialog is already implemented). Any value other than the literal string true is treated as disabled.

Break-Glass Mode (Emergency Bypass)

When TREZOR_SAFEKEEPING_REQUIRED=true but the Trezor device is unavailable (lost, hardware fault, key-holder absent), an admin can activate break-glass mode to temporarily bypass the safekeeping requirement:

Endpoint Action
GET /api/admin/settings/break-glass Read current status (active, expiresAt, activatedBy)
POST /api/admin/settings/break-glass Activate for 1 hour — fires a Telegram alarm immediately
DELETE /api/admin/settings/break-glass Cancel before expiry

Properties:

  • State is in-memory only (resets on server restart — intentional).
  • Activation fires a Telegram alert via tgNotify regardless of TG_NOTIFY_BOT_TOKEN set status.
  • The exported isBreakGlassActive() helper is called by assertTrezorSignatureForOperation — when true, the signature check is skipped.
  • Maximum duration: 1 hour. After expiry the guard is automatically re-enabled.

Source: backend/src/services/admin/breakGlassRoutes.ts (commit b21df25).

Safety Rules

  • Never store Trezor seed words, private keys, or xprv/tprv values.
  • Reject private extended keys at registration.
  • Verify every signature locally before accepting it.
  • Use exact transaction-intent messages; do not accept free-form signatures.
  • Treat generated deposit addresses as public routing metadata, not as proof of payment.
  • Keep ledger availability checks enabled for release/refund accounting.

Upgrade Path To Multisig

The current design stores a single trezor-eoa signer. The recommended production path is to replace the signer policy with:

  • addressType: safe-multisig
  • a Safe address per tenant/admin group
  • threshold policy, such as 2-of-3
  • Trezor owners as Safe signers
  • release/refund flow creates a Safe transaction and records collected signatures before execution

The payment orchestration API should stay the same: build instruction, collect hardware-backed approval, confirm release/refund, append ledger entry. See PRD - Decentralized Custody and Smart-Contract Escrow Roadmap for the staged Safe-first path before any custom escrow contract.