Skip to content

fix(bip137): align recid with the script type - #866

Draft
qlrd wants to merge 2 commits into
selfcustody:developfrom
qlrd:fix/bip137
Draft

fix(bip137): align recid with the script type#866
qlrd wants to merge 2 commits into
selfcustody:developfrom
qlrd:fix/bip137

Conversation

@qlrd

@qlrd qlrd commented May 29, 2026

Copy link
Copy Markdown
Member

What is this PR for?

Minor bug fix.

Previously, the header byte could not reflect the script type's recid when using sparrow <2.5.x for sign messages on some script types (mostly p2sh-p2wpkh). Through some manual experiments with different versions of sparrow , a possibility that a header byte could mismatched to the script type causes verification to fail on those versions.

This commit add a check of a recid as well structure a bip137.py that is strict on context of sparrow 2.5.x and lenient for sparrow <2.5.x (similar what used in electrum clients), as well routes raw hashes to be not signed as bip137 but instead as der one.

Changes made to:

  • Code
  • Tests
  • Docs
  • CHANGELOG

Did you build the code and tested on device?

  • Yes, build and tested on Amigo / Sparrow / krux-file-signer:
    • Sparrow 2.3.1
    • Sparrow 2.4.2
    • Sparrow 2.5.2
    • krux file signer with and without sign [-u] flag

What is the purpose of this pull request?

  • Bug fix
  • New feature
  • Docs update
  • Other

Context

The bip322 will be added to a follow up so the diffs could be simplified

Steps to reproduce (WIP)

Context: built different versions of Sparrow (at least 2 with version <2.5.x) from source and made P2PKH, P2SH_P2WPKH and P2WPKH sign message procedures with a flashed firmware on device with a simple oi (hi on portuguese) on Sparrow on given address.

Below was noted some different behaviours for some different Sparrow's versions. Since used different mnemonics on testnet and testnet4 networks, i think any mnemonic here will be sufficient.

While reproduce expects these results below -- deselecting any mode on Sparrow (Standard(electrum), trezor(BIP137) and BIP322 -- when available):

Sparrow 2.3.1

  • P2PKH: raw / electrum / trezor modes -> all electrum after verify;
  • P2SH-P2PWPKH: raw / electrum / trezor modes -> all trezorBIP137 after verify;
  • P2PWPKH -> raw / electrum / trezor modes -> all electrum after verify;

Sparrow 2.4.2

  • P2PKH: raw / electrum / trezor modes -> all trezorBIP137 after verify;
  • P2SH-P2PWPKH: raw / electrum / trezor modes -> all trezorBIP137 after verify;
  • P2PWPKH -> raw / electrum / trezor modes -> all trezorBIP137 after verify;

Sparrow 2.5.2

  • P2PKH: raw / electrum / trezor modes -> all electrum after verify1;
  • P2SH-P2PWPKH: raw / electrum / trezor modes -> all trezorBIP137 after verify2;
  • P2PWPKH: raw / electrum / trezor modes -> all electrum after verify;

Krux file signer

Download and update to main branch; and run the following command:

uv run poe sign -f README.md # follow the procedures, krux will show `raw hash`.

Then the command below should produce:

uv run poe verify -f README.md -s README.md.sig -p pubkey.pem
Poe => python src/ksigner.py verify -f README.md -s README.md.sig -p pubkey.pem
[HH:MM:ss MM/DD/YY time] Verifying signature:
Signature Verified Successfully

Also need to verify with --uncompressed flag:

uv run poe sign -f README.md -u -s README.md.u.sig
uv run poe verify -f README.md -s README.md.u.sig -p pubkey.pem 
Poe => python src/ksigner.py verify -f README.md -s README.md.sig -p pubkey.pem
[HH:MM:ss MM/DD/YY time] Verifying signature:
Signature Verified Successfully

Note that the -u flag should produce different pubvkeys.

--

Footnotes

  1. Sparrow disables trezor(BIP137) and BIP322 for P2PKH (not disabled on previous versions)

  2. Sparrow disable BIP322 for P2SH_P2WPKH (not disabled on previous versions)

@qlrd
qlrd changed the base branch from main to develop May 29, 2026 00:52
@codecov

codecov Bot commented May 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.48%. Comparing base (649ea07) to head (9b02f88).

Additional details and impacted files
@@           Coverage Diff            @@
##           develop     #866   +/-   ##
========================================
  Coverage    97.48%   97.48%           
========================================
  Files           84       85    +1     
  Lines        10955    10992   +37     
========================================
+ Hits         10679    10716   +37     
  Misses         276      276           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@qlrd qlrd changed the title Fix/bip137 fix(bip137): wallet script type bip137 header May 29, 2026
@qlrd
qlrd force-pushed the fix/bip137 branch 3 times, most recently from e35223d to 4c95cf7 Compare May 29, 2026 13:09
@qlrd
qlrd marked this pull request as ready for review May 29, 2026 13:10

@odudex odudex left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The header-byte fix is correct: single-sig P2SH-P2WPKH and P2WPKH wallets were
emitting legacy-message signatures with the P2PKH-compressed header byte (31-34)
instead of the BIP-137 script-type headers (35-38 / 39-42). Signing now routes
through bip137.sign, which preserves the recovery id and rebases the header on
the script type. Verified against the regenerated vectors:

  • bc1qgl5... (P2WPKH): 32 -> 40 (39 + recid) OK
  • 38CahkV... (P2SH-P2WPKH): 31 -> 35 (35 + recid) OK
  • tb1qynp... (P2WPKH tnet): 32 -> 40 (39 + recid) OK

sign_at emits 31 + recid, so recovery_id = raw_header - 31 is correct and
the re-encoding only swaps the script-type base. Tests pass (5 passed).

Blocking: taproot message signing shouldn't stop working

This PR makes single-sig taproot (m/86') message signing fail. From a user's
perspective this is a regression / introduced bug, not a tightening: taproot
signing worked before and Sparrow verified the result as valid.

Mechanism: get_script_type_from_path("m/86'/...") returns "p2tr", which is
passed to bip137.sign -> build_header. build_header has no p2tr branch, so
it raises ValueError, which the menu catches and shows as an error screen.

Why it really was working (so this is a real loss of function, not removal of a
broken path):

  • Krux signs the standard BIP-137 commitment with an ECDSA-recoverable
    signature using the taproot internal key.
  • Lenient verifiers (Sparrow / Electrum-style) recover the pubkey and
    reconstruct the address of the claimed type from it - for taproot they apply
    the taproot tweak (same as script.p2tr) and the recovered+tweaked key matches
    the bc1p... address. They effectively ignore the header's script-type bits.
  • So the signature genuinely proves control of the taproot key and verifies in
    practice. It's just not a formally specified scheme (BIP-137 has no taproot;
    BIP-322 is the eventual standard).

Required outcome: taproot message signing must keep working. Acceptable options:

  1. Keep the previous behavior - give build_header a p2tr branch that returns a
    valid recoverable header so Krux keeps emitting the Sparrow-verifiable
    signature (this is the no-regression path). The segwit header fix (#865) is
    independent and does not require dropping taproot.
  2. Implement proper BIP-322 signing for taproot.

What is NOT acceptable: shipping a version where taproot signing errors out.

Other required changes

  1. Misleading error message in build_header.
    "%s legacy sign not supported" implies the script type itself is
    unsupported. p2tr is a supported wallet type; the limitation is that BIP-137
    legacy message signing has no taproot scheme. If any reject path remains,
    reword to make that clear and point at BIP-322. Also drop the trailing space
    (currently shown verbatim as ValueError('p2tr legacy sign not supported ')).

  2. Add the MIT license header to src/krux/bip137.py.
    Every other src/krux/*.py file opens with the MIT block; this module starts
    straight at P2PKH_HEADER = 31.

  3. Dead module-level constants. P2PKH_HEADER / P2SH_P2WPKH_HEADER / P2WPKH_HEADER
    are defined but never used; build_header hardcodes the literals 31/35/39.
    Use the constants or drop them so they cannot drift.

  4. Unreachable assertion in test_sign_message_p2tr_bip137.
    The assert ctx.input.wait_for_button.call_count == len(btn_seq) sits after
    the raising call inside the with pytest.raises(...) block, so it never runs.
    Move it after the block or delete it. (This test should change anyway once
    taproot signing is restored.)

Suggested (non-blocking)

  1. Commit says "add test vectors for bip137 module" but there are no direct unit
    tests for bip137.py; coverage is only via the UI. The API-boundary guards in
    build_header / sign are never exercised. Add tests/test_bip137.py.

@qlrd
qlrd marked this pull request as draft June 1, 2026 21:36
@qlrd
qlrd force-pushed the fix/bip137 branch 3 times, most recently from 5d5b2eb to dec7eb1 Compare June 5, 2026 00:12
@odudex

odudex commented Jun 5, 2026

Copy link
Copy Markdown
Member

Why a menu is needed?
Krux should just show the message and sign, everything else should be under the hood.

@qlrd
qlrd force-pushed the fix/bip137 branch 2 times, most recently from 590ba6a to da35069 Compare June 5, 2026 20:41
@odudex

odudex commented Jun 6, 2026

Copy link
Copy Markdown
Member

This needs to be simplified.
For example, there's no need for translated error messages. A simple, single generic raised error is enough.

@qlrd
qlrd force-pushed the fix/bip137 branch 2 times, most recently from 2d7f727 to a8274da Compare June 6, 2026 22:13
@qlrd
qlrd marked this pull request as ready for review June 8, 2026 13:06
@qlrd
qlrd requested a review from odudex June 8, 2026 13:06
@qlrd qlrd changed the title fix(bip137): wallet script type bip137 header fix(bip137): add support to bip322 and fallback to bip137 Jun 8, 2026
@qlrd
qlrd force-pushed the fix/bip137 branch 2 times, most recently from 372d0e0 to e00edad Compare June 8, 2026 13:11
@qlrd
qlrd marked this pull request as draft June 8, 2026 15:23
@qlrd
qlrd marked this pull request as ready for review June 8, 2026 16:40
Comment thread src/krux/bip137.py Outdated

def build_header(raw_sig, script_type):
"""Build header byte from raw signature and script_type"""
recid = raw_sig[0] - P2PKH_HEADER

@jdlcdl jdlcdl Jun 9, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

un-settled thoughts: wouldn't we just left-fshift 6 bits (correction: byte0 & 3) so that we grab the 2 least significant bits as recId?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

from what i found, it need a "normalization" ((byte0 - 27) & 3 -- the p2pkh one), an this works, confirm?

@jdlcdl jdlcdl Jun 15, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I suspect I'm missing an important aspect of bip137, but I'll explain my understanding of at least the header byte, so that I can be corrected on my misunderstanding.

  1. header byte was set by some ecdsa library when the signature was created,
  2. we're just trying to dissect the header so that we know what type of address this is, without modifying the signature,
  3. the 2 least significant bits are the recId and we can have this from any sample_byte between 0-255 via sample_byte & 3 (0b00000011 and any number will result 0-3)
  4. if we were to subtract 27 from the header byte, which has the 2 least-significant bits high/on/one, then we'd be mucking with whatever recId was set by the ec library.

@qlrd qlrd Jun 16, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks for your concerns @jdlcdl (EDIT: i would be glad if i can be corrected on any misunderstanding of mine).

I did that from an interpretation between BitcoinJ sample on BIP137 and ecdsa_sign_recoverable.

From what i understood, on the signing side, embit gave a 65-byte recoverable signature with recId, internally -- (sig + bytes([0..3])).

After, we read that and packed the byte0 = 27 + recId + 4.

So, when raw_sig came with byte0 (27 + recId + 4) — which is why i did (header - 27) & 3 and asked for a review on that.

Comment thread tests/test_bip137.py Outdated
Comment thread tests/test_bip137.py Outdated
@qlrd

qlrd commented Jul 2, 2026

Copy link
Copy Markdown
Member Author

removed all bip322 as suggested before the diff gets big and added some battery of manual tests to reproduce

@qlrd qlrd changed the title fix(bip137): add checks for recid on sign message fix(bip137): align recid with the script type Jul 6, 2026
Copilot AI review requested due to automatic review settings July 6, 2026 19:52

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes BIP-137 message signing header construction so the recovery ID (“recid”) aligns with the script type (notably improving interoperability with older Sparrow versions), and updates message-signing flows to treat raw hashes differently from BIP-137 messages.

Changes:

  • Introduces a dedicated krux/bip137.py module with header construction and message commitment logic.
  • Updates Sign Message UI flows to sign BIP-137 messages via the new module, while routing raw-hash signing to DER signatures.
  • Adds/updates tests to cover strict vs lenient verification behavior and updated UI signature expectations; updates changelog entry.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
tests/test_bip137.py Adds new unit tests for BIP-137 commitment/header behavior and strict/lenient verification expectations.
tests/pages/home_pages/test_sign_message_ui.py Updates UI test vectors to match new signature behavior and adjusts assertions.
src/krux/pages/home_pages/sign_message_ui.py Switches signing paths to use krux.bip137 for non-raw-hash messages and keeps DER signing for raw hashes.
src/krux/bip137.py New module implementing BIP-137 message commitment and header/recid handling.
CHANGELOG.md Notes the recid-related message-signing fix.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/pages/home_pages/test_sign_message_ui.py Outdated
Comment thread tests/test_bip137.py Outdated
Comment thread src/krux/bip137.py Outdated
Comment thread src/krux/bip137.py Outdated
Comment thread src/krux/bip137.py Outdated
Comment thread tests/pages/home_pages/test_sign_message_ui.py Outdated
@qlrd
qlrd marked this pull request as draft July 6, 2026 19:59
@qlrd

qlrd commented Jul 6, 2026

Copy link
Copy Markdown
Member Author

Sorry for copilot messages, forgot to deactivate it

@qlrd

qlrd commented Jul 14, 2026

Copy link
Copy Markdown
Member Author

I think is ready to review with suggested diffless approach, tests and checks. Let me know @odudex @jdlcdl (also prepared a follow-up for bip322).

@odudex odudex left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks good to me!

@qlrd qlrd mentioned this pull request Jul 22, 2026
9 tasks
@qlrd

qlrd commented Jul 29, 2026

Copy link
Copy Markdown
Member Author

@odudex, baked a feat/bip322 based on this one. Do you think it's worth to add it as draft or wait?

@odudex
odudex marked this pull request as ready for review July 29, 2026 16:04
@odudex

odudex commented Jul 29, 2026

Copy link
Copy Markdown
Member

Let's merge this, then you focus on BIP322

@qlrd

qlrd commented Jul 29, 2026

Copy link
Copy Markdown
Member Author

Let's merge this, then you focus on BIP322

Just to document a required review from @joaozinhom (i think his fresh knowledge could be worth of).

@joaozinhom joaozinhom left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Left three inline comments two things I'd change plus a question about the standard-message format. A few small things below, none blocking.

src/krux/bip137.py line 47: the compressed parameter is unreachable in production nothing in krux passes compressed=False, only the tests do. It's also what makes the expression on lines 63–68 hard to read, for a branch that can't be taken. Since this is new API, easier not to ship it than to remove it later.
src/krux/bip137.py line 28: RECID_OFFSET isn't a recid offset, it's the compressed-pubkey offset (+4). Worth renaming now while the module is new.
tests/test_bip137.py lines 153 and 186: print("Case: %d", i) uses a comma instead of %, so it prints the literal %d. Line 230 has it right.

Claude helped me summarize everything and make my text clear.

).digest()

sig = self.ctx.wallet.key.sign_at(derivation, message_hash)
script_type = self.get_script_type_from_path(derivation_str) or "p2pkh"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The recid fix itself is correct — I decoded the new vectors and they line up (KN/4… → byte 40 = P2WPKH_HEADER + 1, IyH8… → 35 = P2SH_P2WPKH_HEADER + 0).

What this line exposes is that the SD flow now decides the script type twice, from two independent sources. _sign_at_address_from_sd reads it from the file on line 217 and uses it to build the address on line 223, but doesn't pass it down on line 225 — so this line re-derives it from the path instead.

Before this PR that didn't matter: the header was always 31–34 and asserted nothing, so a disagreement was invisible. Now the header is authoritative, and the two sources can contradict each other:

path                 file says   path implies   address displayed                            header
m/84h/0h/0h/0/0      p2wpkh      p2wpkh         bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu     39   ok
m/0h/0h/0h/0/0       p2wpkh      None→p2pkh     bc1qaaauaz73eyjyla73u7ahrx0vrlsgf0elwqrhyv     31   ✗

The second row doesn't need a malformed file. _is_valid_derivation_path only checks BIP32 syntax, so it accepts any purpose; get_script_type_from_path then finds no match in SINGLESIG_SCRIPT_PURPOSE (which only has 44/49/84/86), returns None, and the or "p2pkh" fallback here guesses p2pkh while the address on screen is a bc1q…. The .txt we export on lines 341–347 pairs that bech32 address with a header telling the verifier to recover a p2pkh one, so a strict verifier rejects it.

Repro — drop in tests/, passes on this branch:

from .pages.home_pages.test_home import tdata
from .pages import create_ctx


def test_sd_script_type_mismatch(mocker, m5stickv, tdata):
    from krux.pages.home_pages.sign_message_ui import SignMessage
    from krux.wallet import Wallet
    from krux.input import BUTTON_ENTER

    # non-standard purpose; file declares p2wpkh
    file_content = b"msg\nm/0h/0h/0h/0/0\np2wpkh"

    ctx = create_ctx(mocker, [BUTTON_ENTER, BUTTON_ENTER],
                     Wallet(tdata.SINGLESIG_SIGNING_KEY))
    sig, _, address = SignMessage(ctx)._sign_at_address_from_sd(file_content)

    assert address.startswith("bc1q")   # p2wpkh address
    assert 31 <= sig[0] <= 34           # p2pkh header

Nobody loses funds over it, and the common case (Sparrow-generated files) is correct. But the PR does introduce a state where the screen shows one address and the signature claims another with no indication. Giving _sign_at_address an optional script_type and having the SD caller pass the one it already validated on line 218 — falling back to the path otherwise — removes the whole class in about three lines.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Great finding @joaozinhom, i will try to fix this one.

if not self.prompt(t("Proceed?"), BOTTOM_PROMPT_LINE):
return ""
else:
message_hash = bip137.message_commitment(data)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This overwrite makes the label on line 252 inaccurate — the screen still says SHA256: but now renders the BIP-137 commitment, which is sha256(sha256(magic ‖ varint ‖ message)). For b"A test message.":

sha256sum of the content : 60e76ce856c4bd873cc66e424d214ced480d77c34091aad5eb190b2cd9668bd8
what the screen shows    : 6d0485d73f55fe44f8040b0c5e818bebc8319ce3e61970e8f1fb2376858c47ac

That comparison is the user's only check that the device is signing what they think it is before pressing Sign, and the docs promise it explicitly — docs/getting-started/usage/navigating-the-main-menu.en.md line 330: "you will be shown a preview of the message's SHA-256 hash for confirmation before signing". Someone verifying with sha256sum now sees a mismatch with no way to tell whether it's a bug, the wrong file, or intended behaviour.

It's also inconsistent within the same screen: on the is_raw_hash branch the label is still correct, since it shows the 32 bytes the user supplied. Same label, two different kinds of value depending on what was loaded.

Minor, related: _compute_message_hash still computes hashlib.sha256(data).digest() on line 277 and this line discards it two lines later. Folding the commitment into that helper would fix the dead computation and give one place to name the value.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yup, this one have, IMO, a conceptual usage that is more a decision of UI/UX for pleb user flow than tech correctness: the majority of users will not understand the diff between sha256 and sha256(sha256(magic | varint | message)) defined in BIP137; i think those will add more information.

The first path is conceptually lenient; should we accept that we could just call sha256 for normal user even at cost of wrong concept and avoid more explanations (anyway, the user just wanna sign)?

The second path is conceptually strict; it would require add a little translation and diff in docs (what could be fixed in a follow-up if this path is chosen).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Minor, related: _compute_message_hash still computes hashlib.sha256(data).digest() on line 277 and this line discards it two lines later. Folding the commitment into that helper would fix the dead computation and give one place to name the value.

Yup, didn't touched this one i think because could be more than a chore or refactor. Maybe a suggestion or a followup?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

yeah i think this can be a follow up...

if is_raw_hash:
sig = key.sign(message_hash).serialize()
else:
_, sig = bip137.sign(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Question rather than a change request: was the format change on this path intentional? The PR description is about the recid at derived addresses, and this is a separate behavioural change that isn't mentioned.

For non-raw-hash input we used to emit DER over sha256(data); now it's a 65-byte BIP-137 compact signature. Decoding the old and new vectors for case 2 ("hello world") in tests/pages/home_pages/test_sign_message_ui.py line 71:

before:  70 bytes, byte0 = 0x30   -> DER SEQUENCE
after:   65 bytes, byte0 = 0x28   -> BIP-137 header (39 + recid = p2wpkh)

Release signing is unaffected, which I checked: the firmware hash is 64 hex chars, so _compute_message_hash sets is_raw_hash=True and line 260 still produces DER.

The case that changes is signing a plaintext or binary file loaded from SD. That used to yield a signature accepted by openssl sha256 <file -binary | openssl pkeyutl -verify -pubin -inkey pubkey.pem -sigfile sig.bin — the exact command in ./krux verify line 262 — using the hex public key we export right afterwards. A 65-byte compact signature isn't, and navigating-the-main-menu.en.md line 334 still tells users that exported key "can be used by others to verify your signature".

Worth noting too that the header carries no usable meaning here. key.derivation is the account node (src/krux/key.py lines 180–186), so a default singlesig wallet emits header 40, and a strict verifier would recover the pubkey and derive bc1ql5f64jdzjsvgehlpxvdgm9ygp0xta7xpnueh03 — the p2wpkh address of the account node, which no wallet ever displays. For multisig/miniscript key.script_type is p2wsh, which falls through build_header and silently gets a p2pkh header.

If this is wanted, the docs need to follow. If it isn't, keeping BIP-137 to the at-address flow would scope the PR tightly to the bug it names.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Question rather than a change request: was the format change on this path intentional? The PR description is about the recid at derived addresses, and this is a separate behavioural change that isn't mentioned.

Yes, and actually this is what @odudex and @jdlcdl warned me about break firmware sign IIRC. Before i did like you described, since it's a separated behavioral change. But from what i understand, it touches a fundamental part of firmware and a firmware signature isn't the same thing as bip137 signature. Where you think should I point on docs your suggestion?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

docs/getting-started/usage/navigating-the-main-menu.en.md around line 330 — that's where I cited it. And also around line 334 where the openssl pkeyutl verification command is shown, since that command only works for DER over plain SHA-256, not a 65-byte BIP-137 compact signature.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yup,

DER over plain SHA-256, not a 65-byte BIP-137 compact signature.

This was the exact moral barrier to me 🤕

qlrd added 2 commits August 16, 2026 10:33
This commit add checks for signing message UI using recid following spec
from BIP137.
Added unit tests agains BIP137 spec and apply lenient agains strict
checks.
@qlrd qlrd mentioned this pull request Aug 16, 2026
9 tasks
@qlrd
qlrd marked this pull request as draft August 29, 2026 19:14
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.

5 participants