Air-gapped signing: port the QR flow from Parity Signer to multipart UR #10
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Split out of #5, and deferred rather than dropped. Quantus has air-gapped signing; the extension just speaks the wrong protocol for it.
Not a size problem
#5 originally claimed a 5261/7219-byte signature could not fit a QR flow. That was wrong —
@polkadot/react-qralready splits payloads into animated frames:It is a protocol problem
@polkadot/react-qrimplements the Parity Signer / Polkadot Vault wire format:The crypto type is a byte, and there is no ML-DSA value for it. No Quantus cold wallet speaks this format, and no Parity device could sign for a Quantus account even if one did.
Quantus already has both ends
Which is why this is a port rather than a feature:
quantus-clihassigning-qr— "Print the QR a cold wallet scans to sign a call for an account, and stop", with--cold-request-out/--cold-response-infor file-based exchangemobile-app/lib/v2/components/animated_qr_scanner.dart,connect_keystone_screen.dart, with localised strings for frame progress ({scanned}/{total} frames scanned)quantus_urcrate,quantus_sdk/lib/src/utils/ur_qr.dartThe extension is the missing third participant. Doing this would let a user keep keys on a phone or dedicated device and approve dapp transactions from the desktop without the key ever reaching it — the same property the Parity Signer flow provides for Substrate.
Why it is deferred
It is not on the path to signing a first real extrinsic (#7), and it is a self-contained feature that can land any time after. Deferring it means the first working build does not carry a QR flow that cannot work; it does not mean the capability is unwanted.
Scope when picked up
quantus_urandur_qr.dartfor the framing, andquantus signing-qrfor what a request carries@polkadot/react-qr's display and scanner inPopup/Signing/Qr.tsxandPopup/ImportQr.tsxwith UR equivalentsImportQr.tsxcurrently hardcodescreateAccountSuri(name, password, account.content, 'sr25519', …)— an imported cold account is ML-DSA and watch-onlyDistinct from #9, which is address QR and has no protocol involved.
A question this issue needs to answer before the QR-signer UI is removed, found while doing #5.
Removing ImportQr also removes watch-only accounts
createAccountExternal— which callskeyring.addExternaland produces an account with no key — has exactly one caller in the whole codebase:So the "attach external QR-signer account" flow is not merely a way to add a watch-only account, it is the only way. Delete it and the extension can no longer track an address it does not hold keys for — which is a useful capability quite independent of QR, and one nobody asked to lose.
That is why #5 stopped at Ledger and left this alone. The removal is not a cleanup; it is a product decision wearing a cleanup's clothes.
The three options
Option 2 is probably right for the first working build: it drops the protocol that cannot work while keeping the capability that has nothing to do with the protocol. It also pairs naturally with #9 — if the extension can show an address QR, being able to add an address by scanning one is the same feature in reverse, and the mobile app already emits exactly that format.
What is unusable today, either way
ImportQr.tsxhardcodescreateAccountSuri(name, password, account.content, 'sr25519', …)on one branch, and the QR payload it parses is Parity Signer's. An account created through it on a Quantus build is unusable. Whatever is decided, that should not ship as-is.Option 2 taken, on the strength of the capability actually being used:
Landed on
quantus-remove-ledger(6dfd1099). Typecheck, lint and 65 tests clean;build:chromecompletes.One clarification worth recording
The extension has no address book in the polkadot-js/apps sense —
keyring.addressesis never touched here. What it has is external accounts: keyless entries that appear in the account list and are injected into dapps. That is what was at risk, and that is what is preserved.A true contacts list (addresses you send to, distinct from accounts you hold) would be a new feature. Worth knowing if that is what was meant — it is not what this preserved.
What replaced ImportQr
TrackAddresstakes a pasted SS58 string. For Quantus that is the common case anyway: the address is on screen in another wallet, not on a signing device.It round-trips through
decodeAddress/encodeAddress, which does two jobs — rejects a mistyped address by its checksum rather than storing one that can never receive anything, and normalises the prefix so an address pasted from a tool using a different one displays the way the rest of the extension displays it.The bug this would have been
isExternal && !isHardwarepreviously rendered the QR signer. A tracked address satisfies exactly that condition. Left alone, adding a watch-only address and then having a dapp request a signature would have displayed a Parity Signer QR — for an account that can never sign, in a protocol nothing in the Quantus ecosystem can read.External accounts now show the decoded call and say plainly that the extension holds no key for them. Two specs rewritten to assert that rather than the QR scanner.
Signing/Qr.tsxwent with it, along with theCMD_MORTAL/CMD_SIGN_MESSAGEParity command bytes and_onSignature, whose only job was accepting a signature produced outside the extension.Kept for this issue
approveSignSignaturein the background is deliberately left. Unlike Ledger, which is gone for good, external signing comes back when this issue ports the flow to multipart UR — the message it carries is the right shape for that, and deleting it would only mean writing it again.What this issue still needs
The UR port itself. Its scope is unchanged, minus the removal work now done. When picked up, note that the entry point is a different shape than before: scanning to add an address is now #9's territory, and this issue is specifically the signing round trip.