Skip to content

fix(bitbox): sign the registration with a chainId-extended EIP-712 domain - #877

Merged
TaprootFreak merged 3 commits into
RealUnitCH:stagingfrom
Blume1977:fix/bitbox-registration-chainid-domain
Jul 31, 2026
Merged

fix(bitbox): sign the registration with a chainId-extended EIP-712 domain#877
TaprootFreak merged 3 commits into
RealUnitCH:stagingfrom
Blume1977:fix/bitbox-registration-chainid-domain

Conversation

@Blume1977

@Blume1977 Blume1977 commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Problem

BitBox users cannot complete the RealUnit registration: on submit, the BitBox02 (Nova) rejects the signing request and shows "typed data has no chain ID" on the device (first affected customer: userData 412822, 23.07.). The firmware requires a chainId member in the EIP712Domain; our registration payload signs the chainId-less domain { name: 'RealUnitUser', version: '1' }. Software wallets sign it regardless, which is why this never surfaced.

Change

  • Eip712Signer.signRegistration includes chainId (value: apiConfig.asset.chainId, already plumbed through) in the EIP712Domain for BitboxCredentials only.
  • Software wallets keep the legacy domain — pinned by the existing golden-signature test, so this path provably does not change.

Pair PR

DFXswiss/api#4542 — merged and live on production since 2026-07-31 16:47Z. Verification there accepts both domain variants, trying the legacy one first, so software wallets are untouched. The API side is already deployed, so this PR no longer has a merge-order prerequisite.

(The original pair PR #4354 was closed unmerged and superseded by #4542.)

Validated end-to-end on production

Run on 2026-07-31 with a real BitBox02 Nova (bb02p-multi, main firmware v9.26.4 — the build measured to refuse the chainId-less envelope) on an iPhone 17e, against api.dfx.swiss:

hop evidence
device signs over BLE no "typed data has no chain ID" screen, no NACK
DFX accepts the extended domain [RealUnitService] RealUnit registration signature matched chainId 1 domain / … at 16:54:28Z
forward to Aktionariat POST /v1/realunit/register/complete → 201, no Failed to forward RealUnit registration
persisted aktionariat_registration row → status = Completed, active = true

This is the scenario the earlier review asked for real-firmware data on: the same device and firmware that produced the NACK now completes a registration end to end.

Note on scope of that evidence: Completed is written after Aktionariat's /registerUser returns 2xx. It proves their registration endpoint accepted the forwarded payload; it does not by itself prove which step re-verifies the EIP-712 signature on their side, so a later step exercising the signature is still worth watching.

Tests

  • new: BitBox path signs with domain.chainId + chainId member in EIP712Domain types
  • existing golden signature (software wallet) unchanged → legacy path frozen
  • wallet package + registration service suites: 83/83 green (62 + 21); flutter analyze: no issues

…main

The BitBox02 firmware refuses to sign typed data whose EIP712Domain has no
chainId and aborts with 'typed data has no chain ID' on the device, so BitBox
users could not complete the registration at all. Hardware wallets now sign
with the chainId-extended domain; software wallets keep the legacy domain
(pinned by the golden signature test) until Aktionariat confirms its
re-verification accepts the extended variant. Pair PR: DFXswiss/api#4354
(accepts both domain variants, must deploy first).

@TaprootFreak TaprootFreak left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wir brauchen mehr daten um sicher zu sein ob das der echte fix ist

Danswar added 2 commits July 31, 2026 14:11
…-PR reference

The new test asserted chainId against the same literal the helper passed and
checked the EIP712Domain members with containsAll, so three wrong implementations
passed it: hardcoding chainId to 1 (breaks every testnet registration), changing
the chainId type to uint64, and reordering the domain members. All three alter the
domain typehash or value, so the API recovers a foreign address and rejects.

Sign with a non-default chainId, assert the value reaching signETHTypedMessage as
well as the JSON, and assert the member list exactly. Verified: each of the three
mutations now fails.

The comment pointed at DFXswiss/api#4354, which was closed unmerged; the shipped
counterpart is #4542.
Replacing .captured.single with indexed access dropped the implicit "signed
exactly once" assertion, so a second signETHTypedMessage call went unnoticed —
on a hardware wallet that is a second on-device confirmation prompt. Restore it
via ..called(1), matching the signCallCount checks in the integration tests.

The comment pointed at eip712_delegation_bitbox_test.dart, which does not exist;
the file is test/integration/eip7702_delegation_bitbox_test.dart.
@Danswar

Danswar commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Completed 3 review passes (conformity + logic) before marking this ready.

Validated end-to-end on production — this is the real-firmware data the earlier review asked for. Same device and firmware that produced the NACK (BitBox02 Nova, main fw v9.26.4) completed a registration against api.dfx.swiss: no chainId warning screen, prod logged registration signature matched chainId 1 domain, and the registration row reached status = Completed. Details in the description.

Fixed during review:

  • the test could not distinguish the chainId parameter from a hardcoded 1, and checked the EIP712Domain members with containsAll — order- and type-blind. Three wrong implementations passed it: hardcoding chainId: 1 (would break every testnet registration), uint256uint64, and reordering the domain members. All three change the domain typehash or value, so the API recovers a foreign address and rejects. The test now runs for chainId 1 and 11155111, asserts the value reaching signETHTypedMessage as well as the JSON, and asserts the member list exactly; each mutation was re-run and now fails.
  • restored the "signed exactly once" guard (..called(1)) that indexed capture access had dropped — a double sign means a second on-device confirmation prompt.
  • the pair-PR reference pointed at fix(realunit): accept registration signatures over a chainId-extended EIP-712 domain DFXswiss/api#4354, which was closed unmerged; the shipped counterpart is fix(realunit): accept registration signatures over a chainId-extended EIP-712 domain DFXswiss/api#4542 (merged, live on prod). Also corrected a comment cross-reference to a non-existent test file and a stale test count in the description.

Note the API side is already deployed, so the original merge-order prerequisite no longer applies.

@Danswar
Danswar marked this pull request as ready for review July 31, 2026 17:47
@Danswar
Danswar requested a review from TaprootFreak July 31, 2026 18:24
@TaprootFreak
TaprootFreak merged commit 35447f6 into RealUnitCH:staging Jul 31, 2026
11 of 15 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.

3 participants