feat(security): harden Inv #8 BIP-137 message-sign with byte-fingerprint + drainer-string refusal (#454) - #524
Merged
Conversation
…int + drainer-string refusal (#454) Two-part hardening for the BIP-137 message-sign surface that adversarial smoke-test script a110 confirmed terminates at the user's eyes on the Ledger Nano OLED — vulnerable to skim, line-1-only, trust-the-agent, and unicode-confusable substitution failure modes. ## (a) Byte-fingerprint preview `SignedBitcoinMessage` and `SignedLitecoinMessage` now carry `messageSha256` — lowercase hex SHA-256 of the exact UTF-8 bytes submitted to the device for signing. The agent surfaces this in the Inv #8 verbatim block so a user concerned about confusable-char substitution can recompute on a separate device: printf '%s' '<message>' | sha256sum ## (c) Drainer-string refusal New `src/security/drainer-pattern.ts` — pure function that scans the message for value-transfer / authorization markers and refuses BEFORE any device interaction. Fires regardless of agent cooperation (a compromised agent can't suppress this by skipping the preview). Two-tier match (case-insensitive substring): - Single-word semantic markers — `transfer` / `authorize` / `grant` / `custody` / `release` / `consent` - Multi-word drainer templates (more specific, take precedence) — "I authorize" / "granting full custody" / "I consent to" / "I hereby transfer" / "release my" Refusal error names the matched pattern + kind so the user sees exactly why it tripped and gets a concrete next step (reword the message or use a non-vaultpilot signing tool). ## False positives are recoverable Legitimate Sign-In-with-Bitcoin / proof-of-funds / proof-of-ownership messages don't typically use these markers — they read "Verify ownership of <addr>" or "Sign in to <site>". A control test asserts a realistic SIWB-shaped message passes through cleanly. If a user genuinely needs to sign a transfer-shaped message, they reword or use a different signing tool — the asymmetric cost (false positive ≈ inconvenience; false negative ≈ fund loss) justifies erring on the side of refusal. ## Scope `sign_message_btc` and `sign_message_ltc` ONLY. Contacts-CRUD signing is structurally fixed (`VaultPilot-contact-v1:` JSON preimage on a controlled shape) and goes through a different path that never hits this check. ## Out of scope (per issue) - Coordinate with Ledger for a message-hash preview line in the BTC app — firmware-vendor work, not this repo. - Heuristic refusal for "embedded address NOT in user's saved contacts" — the issue itself flags this as a stretch and it requires plumbing the contacts blob into the message-sign path. Closes #454. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two-part hardening for the BIP-137 message-sign surface that adversarial smoke-test script a110 confirmed terminates at the user's eyes on the Ledger Nano OLED — vulnerable to skim, line-1-only, trust-the-agent, and unicode-confusable substitution failure modes.
Closes #454.
(a) Byte-fingerprint preview
SignedBitcoinMessageandSignedLitecoinMessagenow carrymessageSha256— lowercase hex SHA-256 of the exact UTF-8 bytes submitted to the device for signing. The agent surfaces this in the Inv #8 verbatim block so a user concerned about confusable-char substitution can recompute on a separate device:(c) Drainer-string refusal
New
src/security/drainer-pattern.ts— pure function that scans the message for value-transfer / authorization markers and refuses BEFORE any device interaction. Fires regardless of agent cooperation (a compromised agent can't suppress this by skipping the preview).transfer,authorize,grant,custody,release,consentI authorize,granting full custody,I consent to,I hereby transfer,release myMatch is case-insensitive substring. Templates take precedence over markers so the refusal error names the most-specific phrase that tripped — more actionable diagnostic than a single-word match.
False-positive control
A test asserts realistic SIWB-shaped messages (
example.com wants you to sign in with your Bitcoin account... Verify ownership for proof-of-funds... Nonce: 12345) pass through cleanly. The asymmetric cost (false positive ≈ inconvenience; false negative ≈ fund loss) justifies erring on the side of refusal.Scope
sign_message_btcandsign_message_ltcONLY, per the issue. Contacts-CRUD signing is structurally fixed (VaultPilot-contact-v1:JSON preimage on a controlled shape) and goes through a different path that never hits this check.Out of scope (per issue)
What changes
src/security/drainer-pattern.ts(new) — pure detector + assertion helper.src/modules/btc/actions.ts— callsassertNoDrainerPattern()before pairing lookup; computes + returnsmessageSha256.src/modules/litecoin/actions.ts— same shape, mirror change.src/index.ts— both tool descriptions updated to advertise the new defenses + new response field.test/drainer-pattern.test.ts(new) — 17 unit tests for the detector + assertion (markers / templates / case-insensitivity / template precedence / error message shape).test/btc-pr4-portfolio-message-sign.test.ts— 5 new tests covering: SHA-256 of ASCII message, SHA-256 of multibyte UTF-8 (café 你好 —), marker-match refusal, template-match refusal, refusal-fires-before-pairing, SIWB false-positive control.Test plan
npm run buildcleannpx tsc --noEmitcleannpx vitest run— all 2441 tests pass locally (22 new + 2419 prior)sign_message_btc({wallet, message: \"Sign in to example.com\"})→ response includesmessageSha256andprintf '%s' 'Sign in to example.com' | sha256summatches itsign_message_btc({wallet, message: \"I authorize transfer of 1 BTC\"})→ throwsMESSAGE-SIGN REFUSED — drainer-pattern template \"i authorize\"...🤖 Generated with Claude Code