Skip to content

Run the baseline pre-sign policy before the pre-approval short-circuit - #944

Merged
kwsantiago merged 2 commits into
mainfrom
mobile-baseline-presign-policy
Aug 4, 2026
Merged

Run the baseline pre-sign policy before the pre-approval short-circuit#944
kwsantiago merged 2 commits into
mainfrom
mobile-baseline-presign-policy

Conversation

@kwsantiago

@kwsantiago kwsantiago commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Installing hooks replaces the set rather than composing with it, so this crate's hooks are the entire pre-sign policy on mobile and the ones the crate provides never run here. Composing is one call, which is how the bundled combination in the same module is written. This invokes it, and then closes the gap that invoking it alone leaves.

Worth being accurate about what was broken, because the obvious reading overstates it. A raw request did not previously sign silently: absent a pre-approval it reached the human prompt and rendered as claimed: raw. The real gap was the pre-approved path, and the value here is defense in depth plus parity with the crate policy rather than closing a silent-signing hole.

Placement is the part worth reviewing. The policy goes above the pre-approval short-circuit, for the same reason the kill switch already sits there: an approval recorded earlier must not authorise a request the policy refuses now. Below the short-circuit, exactly the requests that skip the prompt would also skip the policy, which is the wrong half to exempt. That ordering also turns out to matter for a second reason: consuming a pre-approval removes it, so a policy running afterwards would let a peer burn a user's approval with a request that was never going to be signed.

A denylist on one string is not enough on its own, and review found the case that shows why. An empty label is not "raw", so the comparison misses it, and the approval surface drops a blank label along with the qualifier that marks a label unverified. The request carrying no stated domain at all therefore rendered with fewer warnings than one that admitted to being raw: the most dangerous request was the least alarming. The same divergence covers a label of raw followed by a zero-width character, which the comparison misses and the prompt renders as raw. So the second check refuses anything whose displayed label is blank or raw, normalising exactly the way the prompt does before deciding.

Requiring a structured body on every request is the stronger policy and is deliberately not applied. The payload is optional at the request site, so mandating it would refuse initiators that do not attach one, and a co-signer that cannot sign reads as broken rather than as protected.

One behavior change for release notes: the CLI's frost network sign does send the raw label, so a phone in that group now refuses it. That is not fatal. The coordinator excludes the index and fails over, erroring only when the phone's share is needed to reach threshold. Mobile enforcing this unconditionally while serve --refuse-raw-sign stays opt-in is a deliberate asymmetry: a phone holding key material is the right place to be stricter than a server default.

Test plan

Five tests. The first stages a raw request that is already pre-approved, which is the case that distinguishes the two placements, since a pre-approval records that the user accepted one specific set of bytes rather than consent to sign an unstructured request. Two cover the displayed-label guard: a label that renders blank, across empty, whitespace and zero-width inputs, and raw spelled with a zero-width character. One covers a raw request with no pre-approval. The last checks a pre-approved request with an ordinary label still passes, so that refusing everything cannot satisfy any of the others.

Falsified rather than assumed, twice. Moving the policy below the short-circuit makes the first fail while the last still passes. Removing the displayed-label guard makes both of its tests fail while the other three pass. That second run also showed why the not-pre-approved case earns its place: without the guard those tests fall through to the prompt and wait out the full sixty-second response timeout instead of failing, and a regression that stalls the suite is harder to read than one that fails.

Formatter and clippy clean.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

MobileSigningHooks::pre_sign now applies raw-signature refusal before pre-sign approvals. Tests verify raw requests remain rejected and pre-approved Bitcoin sighash requests remain accepted.

Changes

Raw signature policy

Layer / File(s) Summary
Apply refusal before approval
keep-mobile/src/lib.rs
pre_sign invokes RefuseRawSignatureHooks before checking pre-approvals. Tests cover raw-request rejection and successful pre-approved Bitcoin sighash requests.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: wksantiago

Poem

A rabbit checks the signing gate,
Raw labels meet a firm “not yet.”
Approved sighash hops through bright,
While tests keep watch both day and night.
Policy first, then approval’s light.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 82.35% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: applying the baseline pre-sign policy before the pre-approval short-circuit.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch mobile-baseline-presign-policy

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@kwsantiago

Copy link
Copy Markdown
Contributor Author

Review found the case that made the first version of this incomplete, and it is worth stating plainly because the shape recurs: a denylist on one string leaves the label that is not on the list.

An empty label is not "raw", so the comparison misses it. Worse, the approval surface drops a blank label entirely, and the qualifier marking a label unverified goes with it. So the request carrying no stated domain at all rendered with fewer warnings than one that admitted to being raw. Refusing the honest version while passing the anonymous one is the wrong way round, and closing only half of it would have read as a fix.

The same divergence covers "raw" followed by a zero-width character. The upstream comparison trims Unicode whitespace, which does not include zero-width space, while the prompt strips it; the label evades the check and still renders as "raw". Both cases come from the same root: the thing that decides and the thing that displays disagree about what the label says. The second check therefore normalises exactly the way the prompt does and refuses anything whose displayed label is blank or raw, so the property is stated in terms of what the user will actually see.

Two corrections to my own framing, both from the review. A raw request did not previously sign silently: without a pre-approval it reached the prompt showing "claimed: raw", so the genuine gap was the pre-approved path and this is defense in depth rather than a silent-signing fix. And the comment claiming nothing legitimately sends the raw label to a phone was wrong: the CLI's frost network sign does. Both are corrected in the body.

Also worth recording, since it validates a point the review made rather than mine: falsifying the new guard reproduced the stall it predicted. With the check removed, those two tests fall through to the prompt and wait out the full sixty-second response timeout instead of failing. That is why the raw-without-pre-approval case is in the suite: it is not coverage for its own sake, it is the difference between a regression that fails and one that hangs.

Filed rather than fixed here: refusals are not audited anywhere, so a phone owner has no durable record that a peer probed with raw requests, and the same applies to the kill switch. A pre-approval can also still be burned without a signature through paths that return early after the hook runs. Both are pre-existing and neither is made worse by this change.

@kwsantiago
kwsantiago merged commit f736cc1 into main Aug 4, 2026
12 checks passed
@kwsantiago
kwsantiago deleted the mobile-baseline-presign-policy branch August 4, 2026 21:56
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.

1 participant