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>
153 lines
7.8 KiB
Markdown
153 lines
7.8 KiB
Markdown
> **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.tsx` → `TrezorSettingsView` (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 `TrezorSignDialog` → `actions/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=...®istrationAddress=...`
|
|
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:
|
|
|
|
```http
|
|
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:
|
|
|
|
```text
|
|
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:
|
|
|
|
```http
|
|
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:
|
|
|
|
```json
|
|
{
|
|
"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
|
|
|
|
```env
|
|
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.
|