Files
nick-doc/UAT - Trezor Safekeeping (Task #11).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

14 KiB

UAT — Trezor Safekeeping (Task #11)

Feature: Hardware-protected admin release/refund actions via Trezor signing
Branch: integrate-main-into-development
Backend version: 2.6.59
Tester role required: Admin
Browser requirement: Chromium-based only (Chrome, Edge, Brave) — WebUSB is not supported in Firefox or Safari


Overview

Task #11 adds a hardware security layer over two critical admin actions:

Action Without Trezor With Trezor
Release payment to seller Admin clicks → tx hash → confirm Admin clicks → Trezor signs → tx hash → confirm
Refund payment to buyer Admin clicks → tx hash → confirm Admin clicks → Trezor signs → tx hash → confirm

The Trezor requirement is controlled by the backend env var TREZOR_SAFEKEEPING_REQUIRED. When false, the entire Trezor layer is transparent and the UI behaves exactly as before.

An emergency break-glass mechanism lets an admin temporarily bypass Trezor for 1 hour with a mandatory Telegram alarm.


Flows Implemented

Flow A — Trezor Registration (one-time setup)

URL: /dashboard/admin/trezor
When: First time, or when replacing the registered device.

Steps:

  1. Admin opens the Trezor Safekeeping page.
  2. (Optional) Admin enters a device label in the text field.
  3. Admin clicks Connect Trezor.
  4. Browser shows a USB device picker — admin selects the Trezor.
  5. Trezor device shows "Export public key?" — admin confirms on device.
  6. Backend issues a registration message tied to the xpub + address.
  7. UI moves to Sign step — admin clicks Sign on Trezor.
  8. Trezor device shows the message to sign — admin confirms on device.
  9. UI shows "Signature obtained — ready to register."
  10. Admin clicks Register Trezor → backend stores xpub fingerprint + registration address.
  11. Page refreshes and shows the registered account status (address, fingerprint, path, address count).

If a Trezor is already registered, the page shows a Re-register section instead of the initial registration section. The flow is identical; it overwrites the old record.


Flow B — Release Payment (with Trezor)

URL: /dashboard/admin/payments-awaiting-confirmation
Precondition: TREZOR_SAFEKEEPING_REQUIRED=true, break-glass inactive, Trezor registered.

Steps:

  1. Admin opens the Payments Awaiting Confirmation list.
  2. Clicks the green Release button on a payment row.
  3. Dialog opens at step 1 — Build Instruction.
    • Shows payment ID and amount.
    • Admin clicks Build Instruction.
  4. Backend builds the release instruction (validates payment state).
  5. Dialog advances to step 2 — Sign with Trezor.
    • Shows provider and network info from the instruction.
    • Admin connects Trezor (if not already connected) and clicks Sign on Trezor.
  6. Trezor device shows the operation message — admin confirms on device.
  7. Dialog advances to step 3 — Enter tx hash.
    • Shows "Trezor approved — signer: 0x…" confirmation.
    • Admin pastes the on-chain transaction hash of the actual transfer.
    • Admin clicks Next.
  8. Dialog advances to step 4 — Confirm.
    • Admin clicks Confirm Release (green).
  9. Backend verifies the Trezor signature against the registered xpub, records the tx hash, and marks the payment released.
  10. Dialog closes, list refreshes.

Flow C — Refund Payment (with Trezor)

URL: /dashboard/admin/payments-awaiting-confirmation
Precondition: Same as Flow B.

Identical to Flow B except:

  • Admin clicks the orange Refund button.
  • Step 1 shows an optional Refund reason text field.
  • Final button is Confirm Refund (orange).
  • reason is passed through to the confirm API if provided.

Flow D — Release / Refund without Trezor (break-glass active or safekeeping disabled)

Precondition: Either TREZOR_SAFEKEEPING_REQUIRED=false OR break-glass is active.

The dialog runs a shorter 3-step flow:

Step Label Action
1 Build instruction Same as above
2 Enter tx hash No Trezor prompt; shows "Break-glass mode active" notice
3 Confirm Submits without trezor field in body

The backend accepts the request because isTrezorSafekeepingRequired() returns false in both cases.


Flow E — Break-glass Activation

URL: /dashboard/admin/trezor
When: Emergency — Trezor is unavailable, lost, or broken.

Steps:

  1. Admin opens the Trezor Safekeeping page.
  2. The Break-glass section shows "Trezor safekeeping is active (normal mode)".
  3. Admin clicks Activate Break-glass.
  4. Confirmation dialog appears with warning text.
  5. Admin clicks Activate.
  6. Backend activates break-glass for 1 hour and fires a Telegram alarm to the monitoring bot (TG_NOTIFY_BOT_TOKEN).
  7. Page shows red alert: "Break-glass is ACTIVE — Activated by: [email] — Expires: [time] — Remaining: ~60 min".
  8. The payments-awaiting-confirmation list now passes skipTrezor={true} to the dialog.

Flow F — Break-glass Deactivation

URL: /dashboard/admin/trezor

Steps:

  1. Admin opens the Trezor Safekeeping page while break-glass is active.
  2. The section shows the red active alert with remaining time.
  3. Admin clicks Deactivate Break-glass.
  4. Confirmation dialog appears.
  5. Admin clicks Deactivate.
  6. Backend clears break-glass immediately and fires a Telegram "restored" notification.
  7. Page shows green "Trezor safekeeping is active (normal mode)".

Flow G — Break-glass auto-expiry

No UI action required.

  • After 1 hour, isBreakGlassActive() on the backend returns false automatically.
  • On the next page load or dialog open, getBreakGlassStatus() returns active: false.
  • skipTrezor reverts to false on the payments list.

API Endpoints

Method Path Purpose
GET /api/trezor/account Get registered Trezor status
GET /api/trezor/registration-message Get message to sign for registration
POST /api/trezor/register Submit registration (xpub + proof signature)
POST /api/trezor/operation-message Get message to sign for a release/refund operation
POST /api/payment/:id/release Build release instruction
POST /api/payment/:id/release/confirm Confirm release with tx hash (+ optional trezor sig)
POST /api/payment/:id/refund Build refund instruction
POST /api/payment/:id/refund/confirm Confirm refund with tx hash (+ optional trezor sig)
GET /api/admin/settings/break-glass Get break-glass status
POST /api/admin/settings/break-glass Activate break-glass
DELETE /api/admin/settings/break-glass Deactivate break-glass

Acceptance Tests

AT-01 — Page loads and shows unregistered state

Precondition: No Trezor registered in DB.
Steps: Navigate to /dashboard/admin/trezor as admin.
Expected:

  • "No Trezor registered — admin actions are unprotected" warning is shown.
  • "Register Trezor" card with device label field and Connect Trezor button is visible.
  • Break-glass card shows "Trezor safekeeping is active (normal mode)" in green.

AT-02 — Registration flow completes (hardware path)

Precondition: Trezor Model One or T connected via USB. Chromium browser.
Steps: Follow Flow A above.
Expected:

  • USB device picker appears after clicking Connect Trezor.
  • Trezor device shows public key export prompt.
  • After approval, page moves to Sign step and shows the registration message.
  • Trezor device shows the message to sign.
  • After signing, Submit step shows "Signature obtained".
  • After Submit, page shows account status card with green "Trezor registered" alert.
  • Chips show correct registration address (matches what was shown on device), fingerprint, path m/44'/60'/0', and address count 0.

AT-03 — Registration rejected on device

Precondition: Trezor connected.
Steps: Click Connect Trezor → reject the export on device.
Expected:

  • Toast shows "Trezor getPublicKey failed: …" or similar error.
  • Registration flow resets to idle (Connect button visible again).

AT-04 — Release without Trezor (safekeeping disabled)

Precondition: Backend TREZOR_SAFEKEEPING_REQUIRED=false.
Steps:

  1. Go to /dashboard/admin/payments-awaiting-confirmation.
  2. Click Release on any payment with status awaiting confirmation.

Expected:

  • Dialog opens with 3-step stepper (Build instruction / Enter tx hash / Confirm).
  • Build Instruction succeeds without Trezor prompt.
  • "Break-glass mode active — Trezor signature not required." notice shown in step 1.
  • Admin enters a dummy tx hash and clicks Next → Confirm.
  • POST /api/payment/:id/release/confirm called with txHash but without trezor field.
  • Success toast, dialog closes, list refreshes.

AT-05 — Release with Trezor (full hardware path)

Precondition: TREZOR_SAFEKEEPING_REQUIRED=true, Trezor registered, break-glass inactive, Trezor device connected.
Steps: Follow Flow B above.
Expected:

  • Dialog shows 4-step stepper.
  • Sign with Trezor step shows provider/network info from instruction.
  • Trezor device prompts to sign the operation message.
  • After signing, tx hash step shows "Trezor approved — signer: 0x…".
  • Confirm call includes trezor: { message, signature } in body.
  • Backend returns 200. Toast: "Release confirmed".

AT-06 — Release blocked when Trezor required but not signed

Precondition: TREZOR_SAFEKEEPING_REQUIRED=true, Trezor registered, break-glass inactive.
Steps: POST directly to /api/payment/:id/release/confirm with only txHash, no trezor field.
Expected:

  • Backend returns 403 or 400 with message indicating Trezor signature is required.

AT-07 — Refund with reason

Precondition: TREZOR_SAFEKEEPING_REQUIRED=false (for UI simplicity).
Steps: Follow Flow C. Enter "Item not received" in the reason field.
Expected:

  • Reason field visible in Build Instruction step.
  • POST /api/payment/:id/refund/confirm includes reason: "Item not received".
  • Toast: "Refund confirmed".

AT-08 — Break-glass activation fires Telegram alarm

Precondition: Backend TG_NOTIFY_BOT_TOKEN and TG_NOTIFY_CHAT_ID set.
Steps: Follow Flow E.
Expected:

  • Confirmation dialog warns about 1-hour bypass and Telegram alarm.
  • After clicking Activate, page shows red break-glass active alert.
  • Telegram monitoring chat receives alarm message from the notify bot (NOT the mini app bot).
  • expiresAt shown is approximately 1 hour from now.
  • remainingMs shown is approximately 60 min.

AT-09 — Break-glass makes Trezor step disappear in release dialog

Precondition: TREZOR_SAFEKEEPING_REQUIRED=true, break-glass active.
Steps:

  1. Confirm break-glass is active on Trezor page.
  2. Go to payments-awaiting-confirmation.
  3. Click Release on any payment.

Expected:

  • Dialog shows 3-step stepper (Build instruction / Enter tx hash / Confirm).
  • No "Sign with Trezor" step visible.
  • Step 1 shows "Break-glass mode active — Trezor signature not required."
  • Release completes without Trezor device.

AT-10 — Break-glass deactivation restores Trezor step

Precondition: Break-glass currently active.
Steps:

  1. Go to Trezor page and click Deactivate Break-glass → confirm.
  2. Go to payments-awaiting-confirmation and click Release.

Expected:

  • Trezor page shows green "Trezor safekeeping is active (normal mode)".
  • Telegram monitoring chat receives "break-glass restored" notification.
  • Release dialog shows 4-step stepper again.

AT-11 — Break-glass expires automatically

Precondition: Break-glass activated.
Steps: Wait 1 hour (or manually set BREAK_GLASS_DURATION_MS to a short value in breakGlassRoutes.ts for testing).
Expected:

  • After expiry, GET /api/admin/settings/break-glass returns active: false.
  • Page refresh shows normal mode.
  • Release dialog shows 4-step stepper.

AT-12 — Non-admin cannot access Trezor endpoints

Precondition: Logged in as buyer or seller.
Steps: Call GET /api/trezor/account and GET /api/admin/settings/break-glass with non-admin JWT.
Expected:

  • Both return 403.

AT-13 — Re-registration overwrites old device

Precondition: Trezor already registered.
Steps:

  1. Note the current registration address on the Trezor page.
  2. Click Register New Device, complete registration flow with same or different Trezor.

Expected:

  • Page shows updated registration address after re-registration.
  • If different device: new fingerprint, new address.
  • Previous Trezor's signatures are no longer accepted on subsequent releases.

AT-14 — Cancel at any step resets dialog

Precondition: Any.
Steps: Open release dialog, complete Build Instruction step, click Cancel.
Expected:

  • Dialog closes with no action taken.
  • Reopening the dialog starts at step 1 with empty state.

Test Environment Setup

Minimal (no hardware — UI flow only)

# backend/.env
TREZOR_SAFEKEEPING_REQUIRED=false

Covers: AT-01, AT-04, AT-07, AT-08 to AT-10, AT-12, AT-14

With safekeeping enforced (no hardware — break-glass path)

# backend/.env
TREZOR_SAFEKEEPING_REQUIRED=true
  1. Go to /dashboard/admin/trezor
  2. Activate break-glass
  3. Now release/refund works without Trezor

Covers: AT-09, AT-10

Full hardware path

Requirements:

  • Trezor Model One or Model T
  • USB cable
  • Chromium browser (Chrome / Edge / Brave)
  • TREZOR_SAFEKEEPING_REQUIRED=true
  • Frontend served on https:// or localhost (WebUSB blocks non-secure production origins)

Covers: AT-02, AT-03, AT-05, AT-06, AT-13


Known Limitations / Out of Scope

Item Status
Firefox / Safari WebUSB support Not supported — Trezor registration and signing only works in Chromium
Multiple Trezor accounts Only one active registration at a time; re-register to replace
Break-glass persistence across backend restarts Intentionally not persisted — restart clears break-glass as a security property
Payout / sweep actions via Trezor Backend assertTrezorSignatureForOperation() is wired; no UI yet
Audit log of Trezor operations Not implemented in this task