Severity
High — a Swiss user who enters a normal national number (079… next to prefix +41) can complete the whole wizard and then lose the submit on Abschliessen. Nothing is stored. The snackbar blames «your entries», which points at the tax step they are looking at.
Seen in production: tax-residence step, country Switzerland, server reason Invalid signature. Tax is a red herring. POST /v1/realunit/register/complete verifies the EIP-712 signature before any tax check.
Problem
The app signs whatever PhoneNumberField concatenates:
// lib/widgets/form/phone_number_field.dart
final value = '$prefix$number';
Prefix defaults to +41. The national field accepts any digits, including a leading 0. A typical Swiss mobile therefore becomes +410791234567. That string is what RealUnitRegistrationService._completeRegistration puts in the EIP-712 envelope and in the DTO (toBitboxSafeAscii does not change it).
On the API the same field is rewritten before signature verification:
// DFXswiss/api — RealUnitRegistrationDto.phoneNumber
@Transform(DfxPhoneTransform) // libphonenumber → E.164
Measured against libphonenumber-js:
| Typed / signed |
After DfxPhoneTransform |
isValid |
+41791234567 |
+41791234567 |
yes — hash matches |
+41 79 123 45 67 |
+41791234567 |
yes — hash broken |
+410791234567 |
+41791234567 |
yes — hash broken |
Validation still passes. Recovery of the typed-data signature does not. The API returns 400 Invalid signature.
The number-field hint (1231234567) does not look like a Swiss mobile, so the trunk-0 input is the path a CH user actually takes.
Effect
- Wizard state is local only. Retry with the same number fails the same way.
- The user is on Steueransässigkeit and is told to re-check their entries. Switzerland as tax country is valid (
swissTaxResidence: true, empty countryAndTINs).
- Same class of break for any other signed field the API mutates after receive (
Util.sanitize on name/street/city/ZIP). Phone is the one that fires on ordinary CH input.
Repro
- New registration, prefix
+41, national number 0791234567 (or any valid Swiss mobile with the trunk 0).
- Fill the rest, tax country Switzerland, Abschliessen, sign.
- Snackbar:
Invalid signature. Network: POST /v1/realunit/register/complete → 400.
Control: same flow with national number 791234567 succeeds.
Proposal
Sign the representation the API will keep, and do not let a CH trunk 0 reach the envelope.
- Compose E.164 before
Eip712Signer.signRegistration and use that same string in the DTO. After that, DfxPhoneTransform is a no-op.
- In
PhoneNumberField, when the prefix is a country calling code, drop a leading national trunk 0 (CH 079… → 79…). Keep the API as the authority on whether the number is valid — only normalize form.
- Hint / helper that matches a Swiss mobile (
79 123 45 67), not 1231234567.
- For
Invalid signature specifically, do not use registrationRejected («check your entries»). That copy was written for content-level 4xx and is wrong here.
Do not add an app-side retry that re-signs after a 400. That would be the capability anti-pattern in CONTRIBUTING.
The API half belongs in DFXswiss/api: verify the signature against the raw body, then transform. Until that lands, (1) is what stops CH registrations dying on a leading 0.
Out of scope
Severity
High — a Swiss user who enters a normal national number (
079…next to prefix+41) can complete the whole wizard and then lose the submit on Abschliessen. Nothing is stored. The snackbar blames «your entries», which points at the tax step they are looking at.Seen in production: tax-residence step, country Switzerland, server reason
Invalid signature. Tax is a red herring.POST /v1/realunit/register/completeverifies the EIP-712 signature before any tax check.Problem
The app signs whatever
PhoneNumberFieldconcatenates:Prefix defaults to
+41. The national field accepts any digits, including a leading0. A typical Swiss mobile therefore becomes+410791234567. That string is whatRealUnitRegistrationService._completeRegistrationputs in the EIP-712 envelope and in the DTO (toBitboxSafeAsciidoes not change it).On the API the same field is rewritten before signature verification:
Measured against
libphonenumber-js:DfxPhoneTransformisValid+41791234567+41791234567+41 79 123 45 67+41791234567+410791234567+41791234567Validation still passes. Recovery of the typed-data signature does not. The API returns 400
Invalid signature.The number-field hint (
1231234567) does not look like a Swiss mobile, so the trunk-0input is the path a CH user actually takes.Effect
swissTaxResidence: true, emptycountryAndTINs).Util.sanitizeon name/street/city/ZIP). Phone is the one that fires on ordinary CH input.Repro
+41, national number0791234567(or any valid Swiss mobile with the trunk0).Invalid signature. Network:POST /v1/realunit/register/complete→ 400.Control: same flow with national number
791234567succeeds.Proposal
Sign the representation the API will keep, and do not let a CH trunk
0reach the envelope.Eip712Signer.signRegistrationand use that same string in the DTO. After that,DfxPhoneTransformis a no-op.PhoneNumberField, when the prefix is a country calling code, drop a leading national trunk0(CH079…→79…). Keep the API as the authority on whether the number is valid — only normalize form.79 123 45 67), not1231234567.Invalid signaturespecifically, do not useregistrationRejected(«check your entries»). That copy was written for content-level 4xx and is wrong here.Do not add an app-side retry that re-signs after a 400. That would be the capability anti-pattern in CONTRIBUTING.
The API half belongs in
DFXswiss/api: verify the signature against the raw body, then transform. Until that lands, (1) is what stops CH registrations dying on a leading0.Out of scope
swissTaxResidence/ tax-step derivation (see KYC registration: swissTaxResidence is hardcoded to true and signed #526).registrationForwardingFailed).